Skip to content

Commit 8f030b7

Browse files
committed
feat: added error handler
1 parent 4156f0a commit 8f030b7

File tree

6 files changed

+116
-8
lines changed

6 files changed

+116
-8
lines changed

src/codegen/internal_lib.py.in

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ from .types import (
3535
igraph_adjlist_t,
3636
igraph_arpack_options_t,
3737
igraph_bliss_info_t,
38+
igraph_error_handler_t,
3839
igraph_hrg_t,
3940
igraph_layout_drl_options_t,
4041
igraph_maxflow_stats_t,
@@ -381,5 +382,11 @@ igraph_destroy = _lib.igraph_destroy
381382
igraph_destroy.restype = None
382383
igraph_destroy.argtypes = [POINTER(igraph_t)]
383384

385+
# Error handling
386+
387+
igraph_set_error_handler = _lib.igraph_set_error_handler
388+
igraph_set_error_handler.restype = igraph_error_handler_t
389+
igraph_set_error_handler.argtypes = [igraph_error_handler_t]
390+
384391
# The rest of this file is generated by Stimulus
385392

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
from .types import igraph_error_t
22

3+
__all__ = ("handle_igraph_error_t",)
4+
35

46
def handle_igraph_error_t(code: igraph_error_t) -> None:
57
"""Handles the given igraph error code, raising exceptions appropriately."""
68
if code:
7-
raise RuntimeError(f"igraph returned error code {code}")
9+
from .setup import _get_last_error_state
10+
11+
error_state = _get_last_error_state()
12+
if error_state:
13+
error_state.raise_error()
14+
else:
15+
raise RuntimeError(f"igraph returned error code {code}")

src/igraph_ctypes/_internal/lib.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
igraph_adjlist_t,
3636
igraph_arpack_options_t,
3737
igraph_bliss_info_t,
38+
igraph_error_handler_t,
3839
igraph_hrg_t,
3940
igraph_layout_drl_options_t,
4041
igraph_maxflow_stats_t,
@@ -381,6 +382,12 @@ def _load_igraph_c_library():
381382
igraph_destroy.restype = None
382383
igraph_destroy.argtypes = [POINTER(igraph_t)]
383384

385+
# Error handling
386+
387+
igraph_set_error_handler = _lib.igraph_set_error_handler
388+
igraph_set_error_handler.restype = igraph_error_handler_t
389+
igraph_set_error_handler.argtypes = [igraph_error_handler_t]
390+
384391
# The rest of this file is generated by Stimulus
385392

386393
# Set up aliases for all enum types
Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,99 @@
1+
from dataclasses import dataclass
2+
3+
from .functions import igraph_strerror
4+
from .lib import igraph_set_error_handler
15
from .rng import NumPyRNG
6+
from .types import igraph_error_t, igraph_error_handler_t
27

38
__all__ = ("setup_igraph_library",)
49

510

6-
def setup_igraph_library() -> None:
7-
"""Initializes the random number generator of the igraph library.
11+
@dataclass
12+
class IgraphErrorState:
13+
"""Dataclass storing the details of the last error message received from
14+
igraph.
15+
"""
816

9-
This function is called when the ``igraph_ctypes`` module is imported by the user.
17+
message: bytes = b""
18+
filename: bytes = b""
19+
line: int = 0
20+
error: igraph_error_t = igraph_error_t(0)
21+
22+
def _error_handler(
23+
self, message: bytes, filename: bytes, line: int, error: igraph_error_t
24+
):
25+
self.message = message
26+
self.filename = filename
27+
self.line = line
28+
self.error = error
29+
print(repr(self))
30+
31+
@property
32+
def has_error(self) -> bool:
33+
"""Returns whether the error state object currently stores an error."""
34+
return int(self.error) != 0
35+
36+
def raise_error(self) -> None:
37+
"""Raises an appropriate Python exception if there is an error stored
38+
in the state object. No-op otherwise.
39+
"""
40+
code = int(self.error)
41+
42+
if code == 0:
43+
return
44+
elif code == 12: # IGRAPH_UNIMPLEMENTED
45+
exc = NotImplementedError
46+
elif code == 2: # IGRAPH_ENOMEM
47+
exc = MemoryError
48+
else:
49+
exc = RuntimeError
50+
51+
msg = self.message
52+
if msg and msg[-1] not in b".!?":
53+
msg = msg + b"."
54+
55+
filename_str = self.filename.decode("utf-8", errors="replace")
56+
message_str = msg.decode("utf-8", errors="replace")
57+
error_code_str = igraph_strerror(self.error).decode("utf-8", errors="replace")
58+
59+
raise exc(
60+
f"Error at {filename_str}:{self.line}: {message_str} -- {error_code_str}"
61+
)
62+
63+
64+
_last_error = IgraphErrorState()
65+
66+
67+
@igraph_error_handler_t
68+
def _error_handler(message: bytes, filename: bytes, line: int, error: igraph_error_t):
69+
global _last_error
70+
_last_error._error_handler(message, filename, line, error)
71+
72+
73+
def _get_last_error_state() -> IgraphErrorState:
74+
global _last_error
75+
return _last_error
76+
77+
78+
def _setup_error_handler() -> None:
79+
"""Sets up the error handler needed to integrate igraph's error handling
80+
nicely with Python.
1081
"""
82+
igraph_set_error_handler(_error_handler)
83+
84+
85+
def _setup_rng() -> None:
86+
"""Initializes the random number generator of the igraph library."""
1187
from numpy.random import default_rng
1288

1389
NumPyRNG(default_rng()).attach()
90+
91+
92+
def setup_igraph_library() -> None:
93+
"""Integrates the facilities of the igraph library with Python.
94+
95+
This function is called when the ``igraph_ctypes`` module is imported by the user.
96+
You should not need to call this function directly.
97+
"""
98+
_setup_error_handler()
99+
_setup_rng()

src/igraph_ctypes/_internal/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,8 @@ class igraph_rng_t(Structure):
409409
]
410410

411411

412+
igraph_error_handler_t = CFUNCTYPE(None, c_char_p, c_char_p, c_int, igraph_error_t)
413+
412414
igraph_isocompat_t = CFUNCTYPE(
413415
igraph_bool_t,
414416
POINTER(igraph_t),

test/test_basic_interface.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from numpy import array
2+
from pytest import raises
23

34
from igraph_ctypes._internal.enums import NeighborMode
45
from igraph_ctypes.constructors import create_empty_graph
@@ -129,11 +130,8 @@ def test_get_eid_directed():
129130
assert g.get_edge_id((i + 1) % n, i, error=False) == -1
130131
assert g.get_edge_id((i + 1) % n, i, directed=False) == i
131132

132-
# TODO(ntamas): does not work yet, error handler must be implemented
133-
"""
134-
with raises(match="no such edge"):
133+
with raises(RuntimeError, match="no such edge"):
135134
assert g.get_edge_id((i + 1) % n, i) == i
136-
"""
137135

138136

139137
def test_get_eid_undirected():

0 commit comments

Comments
 (0)