Skip to content

Commit 039d5ee

Browse files
committed
Improve unsafe setters handling
1 parent dc791ba commit 039d5ee

File tree

5 files changed

+123
-0
lines changed

5 files changed

+123
-0
lines changed

hypergraphx/core/base.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import copy
2+
import os
3+
import warnings
24

35
from hypergraphx.exceptions import (
46
InvalidParameterError,
@@ -45,6 +47,10 @@ def populate_from_dict(self, data):
4547
self._empty_edges = data.get("empty_edges", {})
4648
self._populate_adjacency_data(data)
4749
self._populate_extra_data(data)
50+
# If the implementation provides invariant validation, run it optionally.
51+
maybe_validate = getattr(self, "_maybe_validate_invariants", None)
52+
if callable(maybe_validate):
53+
maybe_validate()
4854

4955
def expose_attributes_for_hashing(self):
5056
edges = []
@@ -165,6 +171,107 @@ def _validate_metadata_dict(self, metadata, label):
165171
if not isinstance(metadata, dict):
166172
raise InvalidParameterError(f"{label} metadata must be a dict.")
167173

174+
# Invariants / debug helpers
175+
def _debug_invariants_enabled(self) -> bool:
176+
"""
177+
Invariant checks can be expensive. Enable them explicitly via:
178+
- running Python without -O (i.e. __debug__ is True) AND
179+
- setting HGX_DEBUG_INVARIANTS=1/true/yes/on.
180+
"""
181+
if not __debug__:
182+
return False
183+
val = os.getenv("HGX_DEBUG_INVARIANTS", "")
184+
return val.strip().lower() in {"1", "true", "yes", "on"}
185+
186+
def validate_invariants(self) -> None:
187+
"""Public hook to validate internal consistency (useful in debugging)."""
188+
self._validate_invariants()
189+
190+
def _validate_invariants(self) -> None:
191+
"""
192+
Validate internal data-structure consistency.
193+
194+
This is intended for debugging and test/dev environments.
195+
"""
196+
# Edge id <-> edge key bijection.
197+
if len(self._edge_list) != len(self._reverse_edge_list):
198+
raise RuntimeError(
199+
"Invariant violated: edge_list and reverse_edge_list size mismatch."
200+
)
201+
for edge_key, edge_id in self._edge_list.items():
202+
if self._reverse_edge_list.get(edge_id) != edge_key:
203+
raise RuntimeError(
204+
"Invariant violated: edge_id <-> edge_key mapping is not a bijection."
205+
)
206+
207+
valid_edge_ids = set(self._reverse_edge_list.keys())
208+
209+
# Weights and edge metadata must align to existing edge_ids.
210+
for edge_id in self._weights.keys():
211+
if edge_id not in valid_edge_ids:
212+
raise RuntimeError(
213+
"Invariant violated: weights contain unknown edge_id."
214+
)
215+
for edge_id in self._edge_metadata.keys():
216+
if edge_id not in valid_edge_ids:
217+
raise RuntimeError(
218+
"Invariant violated: edge_metadata contain unknown edge_id."
219+
)
220+
221+
# Adjacency lists contain only valid edge_ids and only reference known nodes.
222+
for name, adj in self._adjacency_maps().items():
223+
for node, edge_ids in adj.items():
224+
if node not in self._node_metadata:
225+
raise RuntimeError(
226+
f"Invariant violated: adjacency map {name!r} references unknown node."
227+
)
228+
for edge_id in edge_ids:
229+
if edge_id not in valid_edge_ids:
230+
raise RuntimeError(
231+
f"Invariant violated: adjacency map {name!r} contains unknown edge_id."
232+
)
233+
234+
# Incidence metadata should refer to existing edges/nodes.
235+
for (edge_key, node), meta in self._incidences_metadata.items():
236+
if edge_key not in self._edge_list:
237+
raise RuntimeError(
238+
"Invariant violated: incidence metadata references unknown edge_key."
239+
)
240+
if node not in self._node_metadata:
241+
raise RuntimeError(
242+
"Invariant violated: incidence metadata references unknown node."
243+
)
244+
if meta is not None and not isinstance(meta, dict):
245+
raise RuntimeError(
246+
"Invariant violated: incidence metadata must be a dict."
247+
)
248+
249+
def _maybe_validate_invariants(self) -> None:
250+
if self._debug_invariants_enabled():
251+
self._validate_invariants()
252+
253+
def _allow_unsafe_setters(self) -> bool:
254+
val = os.getenv("HGX_ALLOW_UNSAFE_SETTERS", "")
255+
return val.strip().lower() in {"1", "true", "yes", "on"}
256+
257+
def _guard_unsafe_setter(self, name: str) -> None:
258+
"""
259+
Guard "invariant grenade" setters that can put the object into an inconsistent state.
260+
261+
By default they are blocked. Set HGX_ALLOW_UNSAFE_SETTERS=1 to bypass.
262+
"""
263+
if not self._allow_unsafe_setters():
264+
raise InvalidParameterError(
265+
f"{name} is an unsafe operation and is disabled by default. "
266+
"Use populate_from_dict()/load_* APIs instead, or set "
267+
"HGX_ALLOW_UNSAFE_SETTERS=1 if you really know what you are doing."
268+
)
269+
warnings.warn(
270+
f"{name} is unsafe and deprecated; it may be removed in a future release.",
271+
DeprecationWarning,
272+
stacklevel=3,
273+
)
274+
168275
# Core node methods
169276
def add_node(self, node, metadata=None):
170277
"""Add a node to the hypergraph if it does not already exist."""

hypergraphx/core/directed.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,9 @@ def remove_edges(self, edge_list):
520520
self.remove_edge(edge)
521521

522522
def set_edge_list(self, edge_list):
523+
self._guard_unsafe_setter("DirectedHypergraph.set_edge_list")
523524
self._edge_list = edge_list
525+
self._maybe_validate_invariants()
524526

525527
def get_edge_list(self):
526528
return self._edge_list
@@ -615,6 +617,7 @@ def get_adj_dict(self, source_target):
615617
)
616618

617619
def set_adj_dict(self, adj_dict, source_target):
620+
self._guard_unsafe_setter("DirectedHypergraph.set_adj_dict")
618621
if source_target == "source":
619622
self._adj_source = adj_dict
620623
elif source_target == "target":
@@ -623,6 +626,7 @@ def set_adj_dict(self, adj_dict, source_target):
623626
raise ValueError(
624627
"Invalid value for source_target. Must be 'source' or 'target'."
625628
)
629+
self._maybe_validate_invariants()
626630

627631
# Degree
628632
def degree(self, node, order=None, size=None):

hypergraphx/core/multiplex.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ def get_adj_dict(self):
113113
return self._adj
114114

115115
def set_adj_dict(self, adj_dict):
116+
self._guard_unsafe_setter("MultiplexHypergraph.set_adj_dict")
116117
self._adj = adj_dict
118+
self._maybe_validate_invariants()
117119

118120
def get_incident_edges(self, node):
119121
return super().get_incident_edges(node)
@@ -139,7 +141,9 @@ def get_edge_list(self):
139141
return self._edge_list
140142

141143
def set_edge_list(self, edge_list):
144+
self._guard_unsafe_setter("MultiplexHypergraph.set_edge_list")
142145
self._edge_list = edge_list
146+
self._maybe_validate_invariants()
143147

144148
def get_existing_layers(self):
145149
return self._existing_layers

hypergraphx/core/temporal.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,9 @@ def get_edge_list(self):
347347
return self._edge_list
348348

349349
def set_edge_list(self, edge_list):
350+
self._guard_unsafe_setter("TemporalHypergraph.set_edge_list")
350351
self._edge_list = edge_list
352+
self._maybe_validate_invariants()
351353

352354
def check_edge(self, edge, time):
353355
"""Checks if the specified edge is in the hypergraph.
@@ -555,7 +557,9 @@ def get_adj_dict(self):
555557
return self._adj
556558

557559
def set_adj_dict(self, adj_dict):
560+
self._guard_unsafe_setter("TemporalHypergraph.set_adj_dict")
558561
self._adj = adj_dict
562+
self._maybe_validate_invariants()
559563

560564
# Degree
561565
def degree(self, node, order=None, size=None):

hypergraphx/core/undirected.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,9 @@ def remove_edges(self, edge_list):
218218
self.remove_edge(edge)
219219

220220
def set_edge_list(self, edge_list):
221+
self._guard_unsafe_setter("Hypergraph.set_edge_list")
221222
self._edge_list = edge_list
223+
self._maybe_validate_invariants()
222224

223225
def get_edge_list(self):
224226
return self._edge_list
@@ -278,7 +280,9 @@ def get_adj_dict(self):
278280
return self._adj
279281

280282
def set_adj_dict(self, adj):
283+
self._guard_unsafe_setter("Hypergraph.set_adj_dict")
281284
self._adj = adj
285+
self._maybe_validate_invariants()
282286

283287
def subhypergraph(self, nodes: list):
284288
"""

0 commit comments

Comments
 (0)