Skip to content

Commit fc83dd6

Browse files
committed
feat: first (nonfunctional) prototype of an attribute handler
1 parent 27c3f06 commit fc83dd6

File tree

11 files changed

+358
-36
lines changed

11 files changed

+358
-36
lines changed

src/codegen/run.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ def generate_enums( # noqa: C901
123123
"Degseq": "DegreeSequenceMode",
124124
"EdgeorderType": "EdgeOrder",
125125
"EitType": "EdgeIteratorType",
126+
"ErrorType": "ErrorCode",
126127
"EsType": "EdgeSequenceType",
127128
"FasAlgorithm": "FeedbackArcSetAlgorithm",
128129
"FileformatType": "FileFormat",

src/igraph_ctypes/_internal/attributes.py

Lines changed: 214 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
1-
from ctypes import c_char_p, c_int, c_void_p, CFUNCTYPE, POINTER, Structure
1+
from ctypes import (
2+
c_char_p,
3+
c_int,
4+
c_void_p,
5+
pointer,
6+
py_object,
7+
CFUNCTYPE,
8+
POINTER,
9+
Structure,
10+
)
11+
from dataclasses import dataclass, field, replace
12+
from typing import Any, Callable, Dict, Optional
213

14+
from .refcount import incref, decref, refcount
315
from .types import (
416
igraph_bool_t,
517
igraph_error_t,
@@ -14,6 +26,9 @@
1426
igraph_vector_t,
1527
igraph_vs_t,
1628
)
29+
from .utils import protect
30+
31+
__all__ = ("AttributeHandlerBase", "DictAttributeHandler")
1732

1833

1934
p_igraph_t = POINTER(igraph_t)
@@ -94,26 +109,32 @@ class igraph_attribute_table_t(Structure):
94109
"get_type": CFUNCTYPE(
95110
igraph_error_t, p_igraph_t, POINTER(c_int), c_int, c_char_p
96111
),
97-
"get_numeric_graph_attr": CFUNCTYPE(p_igraph_t, c_char_p, p_vector_t),
98-
"get_string_graph_attr": CFUNCTYPE(p_igraph_t, c_char_p, p_strvector_t),
99-
"get_bool_graph_attr": CFUNCTYPE(p_igraph_t, c_char_p, p_vector_bool_t),
112+
"get_numeric_graph_attr": CFUNCTYPE(
113+
igraph_error_t, p_igraph_t, c_char_p, p_vector_t
114+
),
115+
"get_string_graph_attr": CFUNCTYPE(
116+
igraph_error_t, p_igraph_t, c_char_p, p_strvector_t
117+
),
118+
"get_bool_graph_attr": CFUNCTYPE(
119+
igraph_error_t, p_igraph_t, c_char_p, p_vector_bool_t
120+
),
100121
"get_numeric_vertex_attr": CFUNCTYPE(
101-
p_igraph_t, c_char_p, igraph_vs_t, p_vector_t
122+
igraph_error_t, p_igraph_t, c_char_p, igraph_vs_t, p_vector_t
102123
),
103124
"get_string_vertex_attr": CFUNCTYPE(
104-
p_igraph_t, c_char_p, igraph_vs_t, p_strvector_t
125+
igraph_error_t, p_igraph_t, c_char_p, igraph_vs_t, p_strvector_t
105126
),
106127
"get_bool_vertex_attr": CFUNCTYPE(
107-
p_igraph_t, c_char_p, igraph_vs_t, p_vector_bool_t
128+
igraph_error_t, p_igraph_t, c_char_p, igraph_vs_t, p_vector_bool_t
108129
),
109130
"get_numeric_edge_attr": CFUNCTYPE(
110-
p_igraph_t, c_char_p, igraph_es_t, p_vector_t
131+
igraph_error_t, p_igraph_t, c_char_p, igraph_es_t, p_vector_t
111132
),
112133
"get_string_edge_attr": CFUNCTYPE(
113-
p_igraph_t, c_char_p, igraph_es_t, p_strvector_t
134+
igraph_error_t, p_igraph_t, c_char_p, igraph_es_t, p_strvector_t
114135
),
115136
"get_bool_edge_attr": CFUNCTYPE(
116-
p_igraph_t, c_char_p, igraph_es_t, p_vector_bool_t
137+
igraph_error_t, p_igraph_t, c_char_p, igraph_es_t, p_vector_bool_t
117138
),
118139
}
119140

@@ -139,3 +160,186 @@ class igraph_attribute_table_t(Structure):
139160
("get_string_edge_attr", TYPES["get_string_edge_attr"]),
140161
("get_bool_edge_attr", TYPES["get_bool_edge_attr"]),
141162
]
163+
164+
165+
################################################################################
166+
167+
168+
class AttributeHandlerBase:
169+
"""Base class for igraph attribute handlers."""
170+
171+
_table: Optional[igraph_attribute_table_t] = None
172+
_table_ptr = None
173+
174+
def _get_attribute_handler_functions(self) -> Dict[str, Callable]:
175+
"""Returns an ``igraph_attribute_table_t`` instance that can be used
176+
to register this attribute handler in the core igraph library.
177+
"""
178+
return {
179+
key: igraph_attribute_table_t.TYPES[key](
180+
protect(getattr(self, key, self._nop))
181+
)
182+
for key in igraph_attribute_table_t.TYPES.keys()
183+
}
184+
185+
@property
186+
def _as_parameter_(self):
187+
if self._table_ptr is None:
188+
self._table = igraph_attribute_table_t(
189+
**self._get_attribute_handler_functions()
190+
)
191+
self._table_ptr = pointer(self._table)
192+
return self._table_ptr
193+
194+
@staticmethod
195+
def _nop():
196+
pass
197+
198+
199+
@dataclass(frozen=True)
200+
class _DictAttributeStorage:
201+
"""Dictionary-based storage area for the graph, vertex and edge attributes
202+
of a graph.
203+
"""
204+
205+
graph_attributes: Dict[str, Any] = field(default_factory=dict)
206+
vertex_attributes: Dict[str, Any] = field(default_factory=dict)
207+
edge_attributes: Dict[str, Any] = field(default_factory=dict)
208+
209+
def clear(self) -> None:
210+
"""Clears the storage area, removing all attributes from the
211+
attribute dictionaries.
212+
"""
213+
self.graph_attributes.clear()
214+
self.vertex_attributes.clear()
215+
self.edge_attributes.clear()
216+
217+
def copy(
218+
self,
219+
copy_graph_attributes: bool = True,
220+
copy_vertex_attributes: bool = True,
221+
copy_edge_attributes: bool = True,
222+
):
223+
"""Creates a shallow copy of the storage area."""
224+
return self.__class__(
225+
self.graph_attributes.copy() if copy_graph_attributes else {},
226+
self.vertex_attributes.copy() if copy_vertex_attributes else {},
227+
self.edge_attributes.copy() if copy_edge_attributes else {},
228+
)
229+
230+
231+
_MISSING = object()
232+
233+
234+
def _assign_storage_to_graph(graph, storage: Any = _MISSING):
235+
"""Assigns an attribute storage object to a graph, taking care of
236+
increasing or decreasing the reference count of the storage object if needed.
237+
"""
238+
try:
239+
old_storage = graph.contents.attr
240+
except ValueError:
241+
# No storage yet, this is OK
242+
old_storage = _MISSING
243+
244+
if old_storage is storage:
245+
# Nothing to do
246+
return
247+
248+
if old_storage is not _MISSING:
249+
decref(old_storage)
250+
251+
if storage is not _MISSING:
252+
graph.contents.attr = py_object(incref(storage))
253+
else:
254+
graph.contents.attr = py_object()
255+
256+
257+
def _detach_storage_from_graph(graph):
258+
return _assign_storage_to_graph(graph, _MISSING)
259+
260+
261+
class DictAttributeHandler(AttributeHandlerBase):
262+
"""Attribute handler implementation that stores graph, vertex and edge
263+
attributes in dictionaries.
264+
"""
265+
266+
def init(self, graph, attr):
267+
_assign_storage_to_graph(graph, _DictAttributeStorage())
268+
269+
def destroy(self, graph) -> None:
270+
storage: _DictAttributeStorage = graph.contents.attr
271+
storage.clear()
272+
_detach_storage_from_graph(graph)
273+
274+
def copy(
275+
self,
276+
to,
277+
graph,
278+
copy_graph_attributes: bool,
279+
copy_vertex_attributes: bool,
280+
copy_edge_attributes: bool,
281+
):
282+
try:
283+
storage = incref(
284+
graph.contents.attr.copy(
285+
copy_graph_attributes, copy_vertex_attributes, copy_edge_attributes
286+
)
287+
)
288+
to.contents.attr = py_object(storage)
289+
except Exception as ex:
290+
print(repr(ex))
291+
raise
292+
293+
def add_vertices(self, graph, n: int, attr) -> None:
294+
pass
295+
296+
def permute_vertices(self, graph, to, mapping):
297+
pass
298+
299+
def combine_vertices(self, graph, to, mapping, combinations):
300+
pass
301+
302+
def add_edges(self, graph, edges, attr) -> None:
303+
pass
304+
305+
def permute_edges(self, graph, to, mapping):
306+
pass
307+
308+
def combine_edges(self, graph, to, mapping, combinations):
309+
pass
310+
311+
def get_info(self, graph, gnames, gtypes, vnames, vtypes, enames, etypes):
312+
pass
313+
314+
def has_attr(self, graph, type, name) -> bool:
315+
return False
316+
317+
def get_type(self, graph, type, elemtype, name):
318+
pass
319+
320+
def get_numeric_graph_attr(self, graph, name, value):
321+
pass
322+
323+
def get_string_graph_attr(self, graph, name, value):
324+
pass
325+
326+
def get_boolean_graph_attr(self, graph, name, value):
327+
pass
328+
329+
def get_numeric_vertex_attr(self, graph, name, vs, value):
330+
pass
331+
332+
def get_string_vertex_attr(self, graph, name, vs, value):
333+
pass
334+
335+
def get_boolean_vertex_attr(self, graph, name, vs, value):
336+
pass
337+
338+
def get_numeric_edge_attr(self, graph, name, es, value):
339+
pass
340+
341+
def get_string_edge_attr(self, graph, name, es, value):
342+
pass
343+
344+
def get_boolean_edge_attr(self, graph, name, es, value):
345+
pass

src/igraph_ctypes/_internal/enums.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ class MatrixStorage(IntEnum):
414414
COLUMN_MAJOR = 1
415415

416416

417-
class ErrorType(IntEnum):
417+
class ErrorCode(IntEnum):
418418
"""Python counterpart of an ``igraph_error_type_t`` enum."""
419419

420420
SUCCESS = 0
@@ -559,7 +559,7 @@ class LeadingEigenvectorCommunityHistory(IntEnum):
559559
'EdgeSequenceType',
560560
'EigenAlgorithm',
561561
'ErdosRenyi',
562-
'ErrorType',
562+
'ErrorCode',
563563
'FeedbackArcSetAlgorithm',
564564
'FileFormat',
565565
'FloydWarshallAlgorithm',

src/igraph_ctypes/_internal/errors.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from typing import Optional, Type, Union
2+
3+
from .enums import ErrorCode
14
from .types import igraph_error_t
25

36
__all__ = ("handle_igraph_error_t",)
@@ -11,7 +14,46 @@ def handle_igraph_error_t(code: igraph_error_t) -> None:
1114
error_state = _get_last_error_state()
1215
if error_state:
1316
error_state.raise_error()
14-
elif code == 13: # IGRAPH_INTERRUPTED; does not call error handler
17+
elif code == ErrorCode.INTERRUPTED:
1518
raise KeyboardInterrupt
1619
else:
1720
raise RuntimeError(f"igraph returned error code {code}")
21+
22+
23+
def igraph_error_t_to_python_exception_class(code: int) -> Optional[Type[Exception]]:
24+
"""Converts an igraph error code into an appropriate Python exception class.
25+
26+
See `python_exception_to_igraph_error_t()` for the reverse operation.
27+
"""
28+
if code == 0:
29+
return None
30+
elif code == ErrorCode.UNIMPLEMENTED:
31+
return NotImplementedError
32+
elif code == ErrorCode.ENOMEM:
33+
return MemoryError
34+
else:
35+
return RuntimeError
36+
37+
38+
def python_exception_to_igraph_error_t(
39+
exc: Optional[Union[Exception, Type[Exception]]]
40+
) -> int:
41+
"""Converts a Python exception class into an appropriate igraph error code.
42+
43+
See `igraph_error_t_to_python_exception_class()` for the reverse operation.
44+
"""
45+
# TODO(ntamas): more sophisticated conversion, preserving the
46+
# exception message if possible?
47+
48+
if exc is None:
49+
return 0
50+
51+
if isinstance(exc, Exception):
52+
exc = exc.__class__
53+
54+
if issubclass(exc, NotImplementedError):
55+
return ErrorCode.UNIMPLEMENTED
56+
elif issubclass(exc, MemoryError):
57+
return ErrorCode.ENOMEM
58+
else:
59+
return ErrorCode.FAILURE

src/igraph_ctypes/_internal/functions.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3604,7 +3604,7 @@ def bipartite_game_gnp(n1: int, n2: int, p: float, directed: bool = False, mode:
36043604
return graph, types
36053605

36063606

3607-
def bipartite_game_gnm(n1: int, n2: int, m: int, directed: bool = False, mode: NeighborMode = NeighborMode.ALL) -> Tuple[Graph, BoolArray]:
3607+
def bipartite_game_gnm(n1: int, n2: int, m: int, directed: bool = False, mode: NeighborMode = NeighborMode.ALL, multiple: bool = False) -> Tuple[Graph, BoolArray]:
36083608
"""Type-annotated wrapper for ``igraph_bipartite_game_gnm``."""
36093609
# Prepare input arguments
36103610
c_graph = _Graph()
@@ -3614,9 +3614,10 @@ def bipartite_game_gnm(n1: int, n2: int, m: int, directed: bool = False, mode: N
36143614
c_m = m
36153615
c_directed = any_to_igraph_bool_t(directed)
36163616
c_mode = c_int(mode)
3617+
c_multiple = any_to_igraph_bool_t(multiple)
36173618

36183619
# Call wrapped function
3619-
igraph_bipartite_game_gnm(c_graph, c_types, c_n1, c_n2, c_m, c_directed, c_mode)
3620+
igraph_bipartite_game_gnm(c_graph, c_types, c_n1, c_n2, c_m, c_directed, c_mode, c_multiple)
36203621

36213622
# Prepare output arguments
36223623
graph = _create_graph_from_boxed(c_graph)

src/igraph_ctypes/_internal/lib.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1293,7 +1293,7 @@ def _load_igraph_c_library():
12931293

12941294
igraph_bipartite_game_gnm = _lib.igraph_bipartite_game_gnm
12951295
igraph_bipartite_game_gnm.restype = handle_igraph_error_t
1296-
igraph_bipartite_game_gnm.argtypes = [POINTER(igraph_t), POINTER(igraph_vector_bool_t), igraph_integer_t, igraph_integer_t, igraph_integer_t, igraph_bool_t, igraph_neimode_t]
1296+
igraph_bipartite_game_gnm.argtypes = [POINTER(igraph_t), POINTER(igraph_vector_bool_t), igraph_integer_t, igraph_integer_t, igraph_integer_t, igraph_bool_t, igraph_neimode_t, igraph_bool_t]
12971297

12981298
igraph_bipartite_game = _lib.igraph_bipartite_game
12991299
igraph_bipartite_game.restype = handle_igraph_error_t

0 commit comments

Comments
 (0)