Skip to content

Commit 40c5116

Browse files
committed
Merge branch 'gtf-ontoenv' into gtf-buildingmotif
Brings in ontoenv 0.6.0: the pin, OntoEnv.recover in place of deleting .ontoenv/catalog.pending by hand, and catching UnresolvedImportError instead of every Exception when collecting shape dependencies. Three conflicts, all resolved here and nowhere else: - poetry.lock: deleted on this branch by the uv migration, modified on gtf-ontoenv by the pin bump. Kept deleted. gtf-ontoenv is still poetry and needs its lock; this branch does not have one. - pyproject.toml: the whole poetry -> uv restructure collides. Kept this branch's [project] form and re-expressed the pin there as ontoenv>=0.6.0,<0.7.0, the PEP 440 spelling of gtf-ontoenv's ^0.6.0. uv.lock re-resolved: ontoenv 0.6.0a9 -> 0.6.0, nothing else moved. - shape_collection.py: adjacent-import collision only. gtf-ontoenv added UnresolvedImportError to the ontology_environment import; this branch had grown _guarantee_unique_template_name and get_shape_or_branches in the utils import beside it. Took both.
2 parents 3935bed + c318515 commit 40c5116

4 files changed

Lines changed: 71 additions & 54 deletions

File tree

buildingmotif/dataclasses/shape_collection.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515

1616
from buildingmotif import get_building_motif
1717
from buildingmotif.namespaces import BMOTIF, OWL, SH
18-
from buildingmotif.ontology_environment import OntologyImportsNotFound
18+
from buildingmotif.ontology_environment import (
19+
OntologyImportsNotFound,
20+
UnresolvedImportError,
21+
)
1922
from buildingmotif.utils import (
2023
Triple,
2124
_guarantee_unique_template_name,
@@ -245,8 +248,14 @@ def infer_templates(self, library: "Library") -> None:
245248
dependency_graphs[str(dependency)] = bm.ontology_environment.graph_copy(
246249
str(dependency)
247250
)
248-
except Exception as e:
249-
logging.warning(
251+
except UnresolvedImportError as e:
252+
# Only an import ontoenv knows it could not resolve is expected
253+
# here and skippable -- template inference simply proceeds
254+
# without that dependency's shapes. Anything else (a storage
255+
# error, a malformed IRI) is a real failure and propagates:
256+
# ontoenv >=0.6 types this case precisely so that catching it
257+
# no longer swallows those too.
258+
logger.warning(
250259
f"An ontology could not resolve a dependency on {dependency} ({e}). Check this is loaded into BuildingMOTIF"
251260
)
252261
continue

buildingmotif/ontology_environment.py

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,23 @@
33
from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, Tuple, Union
44

55
import rdflib
6-
from ontoenv import CatalogRecoveryError, OntoEnv
6+
from ontoenv import CatalogRecoveryError, OntoEnv, UnresolvedImportError
77

88
from buildingmotif.database.graph_connection import _is_uuid
99

1010
if TYPE_CHECKING:
1111
from buildingmotif.database.graph_connection import GraphConnection
1212

13+
# ``UnresolvedImportError`` is re-exported so callers can catch an
14+
# unresolvable ``owl:imports`` without importing ontoenv themselves -- this
15+
# module is the one place BuildingMOTIF talks to it.
16+
__all__ = [
17+
"BuildingMOTIFGraphStore",
18+
"OntologyEnvironment",
19+
"OntologyImportsNotFound",
20+
"UnresolvedImportError",
21+
]
22+
1323

1424
class OntologyImportsNotFound(Exception):
1525
"""Raised when one or more owl:imports cannot be resolved."""
@@ -43,7 +53,7 @@ def __init__(
4353
else None
4454
)
4555

46-
# ontoenv >=0.6.0a8 deprecated the `init_from_store` flag in favour of
56+
# ontoenv >=0.6 deprecated the `init_from_store` flag in favour of
4757
# explicit lifecycle entry points. The two cases are not the same call:
4858
#
4959
# - persistent: `connect` *is* "create it or reuse the saved index",
@@ -64,44 +74,42 @@ def __init__(
6474
def _connect_recovering(
6575
path: str, store: Optional["BuildingMOTIFGraphStore"], options: Dict[str, Any]
6676
) -> OntoEnv:
67-
"""``OntoEnv.connect``, clearing a stale interrupted-mutation marker.
68-
69-
**This is a workaround for an ontoenv defect; delete it once ontoenv
70-
stops leaving the marker for tolerable failures.**
77+
"""``OntoEnv.connect``, rebuilding the catalog after an interrupted write.
7178
7279
ontoenv writes ``.ontoenv/catalog.pending`` when it begins a batched
73-
mutation and removes it only when the batch finishes cleanly
74-
(``BatchScope::run``). An *unresolvable* ``owl:imports`` makes the batch
75-
end in error, so the marker survives -- and every later open of that
76-
cache raises ``CatalogRecoveryError``.
77-
78-
That is routine here rather than exceptional: Brick declares eight
79-
imports that do not resolve offline (several 404 even online), so a
80-
single ``Library.from_ontology(Brick)`` permanently poisons a
81-
persistent cache. An ontology with no imports leaves no marker, which
82-
is how this was isolated. ontoenv exposes no recovery call, so the only
83-
way through is to remove the file.
84-
85-
The cost of doing so: after a genuine crash mid-write the marker is a
86-
real signal, and this clears it. That is the trade being made -- one
87-
warned retry, and if the retry also fails the error propagates.
80+
mutation and removes it when the batch commits; finding it at open time
81+
means a process died mid-write, and ontoenv raises
82+
``CatalogRecoveryError`` rather than trust an index that may not
83+
describe every stored graph.
84+
85+
Under ontoenv <0.6 that was a routine condition rather than an
86+
exceptional one -- a merely *unresolvable* ``owl:imports`` also left the
87+
marker, so one ``Library.load(ontology_graph=Brick)`` permanently
88+
poisoned a persistent cache -- and with no recovery call available the
89+
only way through was to delete the marker by hand. 0.6 fixed both ends:
90+
a tolerated missing import now commits cleanly and leaves no marker, and
91+
``OntoEnv.recover`` rebuilds the catalog properly.
92+
93+
So reaching this handler now means a genuinely interrupted write, and
94+
recovery is the supported answer to one. It is taken automatically
95+
because the alternative is a dead end: recovering a custom store
96+
requires passing that store, and ours does not exist until a
97+
``BuildingMOTIF`` has been constructed -- which is what failed. It is
98+
logged at warning level because it rescans every stored graph and is
99+
therefore much slower than a normal open. ontoenv verifies the rebuild
100+
and only clears the marker once the replacement index is published, so
101+
a failed recovery leaves the marker in place and raises.
88102
"""
89103
try:
90104
return OntoEnv.connect(path, graph_store=store, **options)
91105
except CatalogRecoveryError:
92-
marker = Path(path) / ".ontoenv" / "catalog.pending"
93-
if not marker.exists():
94-
raise
95106
logging.getLogger(__name__).warning(
96-
"Clearing stale ontoenv recovery marker at %s. This is normally "
97-
"left behind by an unresolvable owl:imports rather than by an "
98-
"actual interrupted write; if this repeats, the ontology cache "
99-
"at %s may genuinely be damaged and can be deleted.",
100-
marker,
107+
"Rebuilding the ontology catalog at %s after an interrupted "
108+
"write. This rescans every stored ontology and may take a "
109+
"while.",
101110
path,
102111
)
103-
marker.unlink()
104-
return OntoEnv.connect(path, graph_store=store, **options)
112+
return OntoEnv.recover(path, graph_store=store, **options)
105113

106114
def close(self) -> None:
107115
self.env.close()
@@ -144,20 +152,20 @@ def closure_names(self, ontology: str, recursion_depth: int = -1) -> list[str]:
144152
Callers that only need to know *what is in* the closure should use this
145153
rather than discarding :py:meth:`closure_copy`'s first return value.
146154
Measured on the Brick 1.4 closure (15 graphs, ~155k triples), three
147-
reps, on ontoenv 0.6.0a9:
155+
reps, on ontoenv 0.6.0:
148156
149157
- ``list_closure`` -- 0.000s, 0.000s, 0.000s
150-
- ``copy_closure`` -- 3.972s, 3.530s, 3.623s (materializes every time)
151-
- ``get_closure`` -- 2.053s, 2.077s, 2.059s (read-only view)
158+
- ``copy_closure`` -- 4.601s, 4.081s, 3.849s (materializes every time)
159+
- ``get_closure`` -- 2.435s, 2.418s, 2.479s (read-only view)
152160
153161
All three report the same 15 names, so for a name lookup this is free
154162
where the alternatives cost seconds.
155163
156-
The ``get_closure`` shape changed between releases and the numbers are
157-
worth keeping for that reason: on a8 it read 8.846s, 0.000s, 0.000s --
158-
one expensive eager index build, then cached. On a9 it is a flat ~2.05s
159-
per call. Cheaper on first use, but no longer free on repeat, so
160-
"bind once and query many times" is not the win it was on a8.
164+
The ``get_closure`` shape changed across the 0.6 alphas and the numbers
165+
are worth keeping for that reason: on 0.6.0a8 it read 8.846s, 0.000s,
166+
0.000s -- one expensive eager index build, then cached. Since a9 it is
167+
a flat ~2.4s per call. Cheaper on first use, but no longer free on
168+
repeat, so "bind once and query many times" is not the win it was.
161169
"""
162170
return list(self.env.list_closure(ontology, recursion_depth=recursion_depth))
163171

@@ -206,7 +214,7 @@ def ontology_names(self) -> list[str]:
206214
def knows(self, ontology: str) -> bool:
207215
"""Whether ontoenv can resolve ``ontology``.
208216
209-
ontoenv >=0.6.0a8 implements the container protocol, so this is a
217+
ontoenv >=0.6 implements the container protocol, so this is a
210218
direct lookup rather than building the full name list to test one
211219
membership. It is also *broader* than ``ontology in ontology_names()``:
212220
``in`` resolves aliases and source URLs as well as canonical names, so
@@ -225,7 +233,7 @@ def ensure_and_get_closure(
225233
"""Ensure graph is registered in ontoenv, then return its import closure.
226234
227235
The returned graph is materialized and mutable. ontoenv's read-only
228-
``get_closure`` view would avoid the copy, but as of 0.6.0a9 a
236+
``get_closure`` view would avoid the copy, but as of 0.6.0 a
229237
``ViewGraph`` deliberately **does not subclass rdflib.Graph**, so it
230238
cannot be handed back through this signature.
231239

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ dependencies = [
4444
"types-jsonschema>=4.21.0.20240311,<5",
4545
"matplotlib>=3.9.2,<4",
4646
"pandas>=2.2.3,<3",
47-
"ontoenv==0.6.0a9",
47+
"ontoenv>=0.6.0,<0.7.0",
4848
]
4949

5050
# NOTE: the ingress packages (BAC0, openpyxl, netifaces, pytz) are ALSO declared in the

uv.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)