Skip to content

Commit 57d40bd

Browse files
committed
feat: added support for attribute type retrieval and casting
1 parent 1a519d9 commit 57d40bd

File tree

7 files changed

+150
-43
lines changed

7 files changed

+150
-43
lines changed
Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import numpy as np
22

33
from numpy.typing import DTypeLike
4-
from typing import Any, Iterable
4+
from typing import Any, Iterable, Type
55

66
from .enums import AttributeType
77
from ..types import (
@@ -10,13 +10,24 @@
1010
)
1111

1212
__all__ = (
13-
"get_igraph_attribute_type_from_iterable",
14-
"get_numpy_attribute_type_from_iterable",
1513
"igraph_to_numpy_attribute_type",
14+
"iterable_to_igraph_attribute_type",
15+
"iterable_to_numpy_attribute_type",
16+
"python_type_to_igraph_attribute_type",
1617
)
1718

1819

19-
def get_igraph_attribute_type_from_iterable( # noqa: C901
20+
def igraph_to_numpy_attribute_type(type: AttributeType) -> DTypeLike:
21+
"""Converts an igraph attribute type to an equivalent NumPy data type."""
22+
if type is AttributeType.BOOLEAN:
23+
return np_type_of_igraph_bool_t
24+
elif type is AttributeType.NUMERIC:
25+
return np_type_of_igraph_real_t
26+
else:
27+
return np.object_
28+
29+
30+
def iterable_to_igraph_attribute_type(
2031
it: Iterable[Any],
2132
) -> AttributeType:
2233
"""Determines the appropriate igraph attribute type to store all the items
@@ -31,26 +42,18 @@ def get_igraph_attribute_type_from_iterable( # noqa: C901
3142
# Iterable empty
3243
return AttributeType.UNSPECIFIED
3344

34-
best_fit: AttributeType
35-
if isinstance(item, bool):
36-
best_fit = AttributeType.BOOLEAN
37-
elif isinstance(item, (int, float, np.number)):
38-
best_fit = AttributeType.NUMERIC
39-
elif isinstance(item, str):
40-
best_fit = AttributeType.STRING
41-
else:
42-
return AttributeType.OBJECT
43-
45+
best_fit = python_type_to_igraph_attribute_type(type(item))
4446
for item in it:
45-
if isinstance(item, bool):
47+
next_type = python_type_to_igraph_attribute_type(type(item))
48+
if next_type is AttributeType.BOOLEAN:
4649
if best_fit == AttributeType.STRING:
4750
return AttributeType.OBJECT
48-
elif isinstance(item, (int, float, np.number)):
51+
elif next_type is AttributeType.NUMERIC:
4952
if best_fit == AttributeType.STRING:
5053
return AttributeType.OBJECT
5154
else:
5255
best_fit = AttributeType.NUMERIC
53-
elif isinstance(item, str):
56+
elif next_type is AttributeType.STRING:
5457
if best_fit != AttributeType.STRING:
5558
return AttributeType.OBJECT
5659
else:
@@ -59,9 +62,7 @@ def get_igraph_attribute_type_from_iterable( # noqa: C901
5962
return best_fit
6063

6164

62-
def get_numpy_attribute_type_from_iterable(
63-
it: Iterable[Any],
64-
) -> DTypeLike:
65+
def iterable_to_numpy_attribute_type(it: Iterable[Any]) -> DTypeLike:
6566
"""Determines the appropriate NumPy attribute type to store all the items
6667
found in the given iterable as an attribute.
6768
@@ -70,17 +71,33 @@ def get_numpy_attribute_type_from_iterable(
7071
7172
When the iterable is empty, a numeric attribute will be assumed.
7273
"""
73-
attr_type = get_igraph_attribute_type_from_iterable(it)
74+
attr_type = iterable_to_igraph_attribute_type(it)
7475
if attr_type is AttributeType.UNSPECIFIED:
7576
attr_type = AttributeType.NUMERIC
7677
return igraph_to_numpy_attribute_type(attr_type)
7778

7879

79-
def igraph_to_numpy_attribute_type(type: AttributeType) -> DTypeLike:
80-
"""Converts an igraph attribute type to an equivalent NumPy data type."""
81-
if type is AttributeType.BOOLEAN:
82-
return np_type_of_igraph_bool_t
83-
elif type is AttributeType.NUMERIC:
84-
return np_type_of_igraph_real_t
85-
else:
86-
return np.object_
80+
def python_object_to_igraph_attribute_type(obj: Any) -> AttributeType:
81+
"""Converts the given Python object into the most fitting igraph attribute
82+
type.
83+
"""
84+
if isinstance(obj, (bool, np.bool_)):
85+
return AttributeType.BOOLEAN
86+
if isinstance(obj, (int, float, np.number)):
87+
return AttributeType.NUMERIC
88+
if isinstance(obj, str):
89+
return AttributeType.STRING
90+
return AttributeType.OBJECT
91+
92+
93+
def python_type_to_igraph_attribute_type(obj: Type[Any]) -> AttributeType:
94+
"""Converts the given Python type into the most fitting igraph attribute
95+
type.
96+
"""
97+
if issubclass(obj, (bool, np.bool_)):
98+
return AttributeType.BOOLEAN
99+
if issubclass(obj, (int, float, np.number)):
100+
return AttributeType.NUMERIC
101+
if issubclass(obj, str):
102+
return AttributeType.STRING
103+
return AttributeType.OBJECT

src/igraph_ctypes/_internal/attributes/value_list.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111
NoReturn,
1212
Sequence,
1313
Sized,
14+
Type,
1415
TypeVar,
16+
Union,
1517
overload,
1618
)
1719

1820
from .enums import AttributeType
1921
from .utils import (
20-
get_igraph_attribute_type_from_iterable,
22+
iterable_to_igraph_attribute_type,
2123
igraph_to_numpy_attribute_type,
24+
python_type_to_igraph_attribute_type,
2225
)
2326

2427
__all__ = ("AttributeValueList",)
@@ -126,18 +129,43 @@ def __init__(
126129
type = (
127130
AttributeType.NUMERIC
128131
if items is None
129-
else get_igraph_attribute_type_from_iterable(items)
132+
else iterable_to_igraph_attribute_type(items)
130133
)
131134

132135
dtype = igraph_to_numpy_attribute_type(type)
133136
array = np.fromiter(items if items is not None else (), dtype=dtype)
134137

138+
self._fixed_length = bool(fixed_length)
139+
135140
# Now we have a NumPy array, but what we actually want is a chunk of
136141
# memory that we manage ourselves, and a NumPy view on top of it
142+
self._init_with_array(array, type)
143+
144+
def _init_with_array(self, array: NDArray, type: AttributeType):
137145
self._buffer = array
138146
self._items = self._buffer[:]
139147
self._type = type
140-
self._fixed_length = bool(fixed_length)
148+
149+
def cast(self, new_type: Union[AttributeType, Type]) -> None:
150+
"""Converts the type of the attribute represented by this list to the
151+
given type.
152+
153+
Args:
154+
new_type: the new type of the attribute. May be an igraph
155+
AttributeType_ or a Python type. Python types will be
156+
converted to their equivalent igraph attribute types.
157+
"""
158+
if not isinstance(new_type, AttributeType):
159+
new_type = python_type_to_igraph_attribute_type(new_type)
160+
161+
numpy_type = igraph_to_numpy_attribute_type(new_type)
162+
if new_type is AttributeType.STRING and self._type is not AttributeType.STRING:
163+
# Requires special treatment because AttributeType.STRING is
164+
# np.object_ so no conversion would happen by default
165+
new_array = np.array([str(x) for x in self._items], dtype=numpy_type)
166+
else:
167+
new_array = self._items.astype(numpy_type)
168+
self._init_with_array(new_array, new_type)
141169

142170
def compact(self) -> None:
143171
"""Compacts the list in-place, reclaiming any memory that was used
@@ -208,9 +236,7 @@ def __delitem__(self, index: IndexLike) -> None: # noqa: C901
208236
):
209237
del self[...]
210238
else:
211-
tmp = np.delete(self._items, index)
212-
self._buffer = tmp
213-
self._items = self._buffer[:]
239+
self._init_with_array(np.delete(self._items, index), self._type)
214240
return
215241

216242
elif hasattr(index, "__getitem__"):
@@ -220,8 +246,7 @@ def __delitem__(self, index: IndexLike) -> None: # noqa: C901
220246

221247
tmp = np.delete(self._items, index) # type: ignore
222248
if not self.fixed_length:
223-
self._buffer = tmp
224-
self._items = self._buffer[:]
249+
self._init_with_array(tmp, self._type)
225250
return
226251
else:
227252
# Try to delete anyway, check if the length remains the same

src/igraph_ctypes/_internal/conversion.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from os import strerror
1010
from typing import Any, IO, Iterable, Iterator, Optional, Sequence, TYPE_CHECKING
1111

12+
from .attributes.utils import python_type_to_igraph_attribute_type
1213
from .enums import MatrixStorage
1314
from .lib import (
1415
fdopen,
@@ -129,6 +130,7 @@
129130
"iterable_to_igraph_vector_t",
130131
"iterable_to_igraph_vector_t_view",
131132
"iterable_vertex_indices_to_igraph_vector_int_t",
133+
"python_type_to_igraph_attribute_type",
132134
"sequence_to_igraph_matrix_int_t",
133135
"sequence_to_igraph_matrix_int_t_view",
134136
"sequence_to_igraph_matrix_t",
Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,54 @@
1-
from numpy import inf, nan, object_
1+
from numpy import inf, nan, bool_, int_, object_
22

33
from igraph_ctypes._internal.enums import AttributeType
44
from igraph_ctypes._internal.types import (
55
np_type_of_igraph_bool_t,
66
np_type_of_igraph_real_t,
77
)
88
from igraph_ctypes._internal.attributes.utils import (
9-
get_igraph_attribute_type_from_iterable,
10-
get_numpy_attribute_type_from_iterable,
9+
iterable_to_igraph_attribute_type,
10+
iterable_to_numpy_attribute_type,
11+
python_object_to_igraph_attribute_type,
12+
python_type_to_igraph_attribute_type,
1113
)
1214

1315
from pytest import mark
1416

1517

18+
@mark.parametrize(
19+
("input", "expected"),
20+
[
21+
(42, AttributeType.NUMERIC),
22+
(42.0, AttributeType.NUMERIC),
23+
(True, AttributeType.BOOLEAN),
24+
("42", AttributeType.STRING),
25+
([42], AttributeType.OBJECT),
26+
(int_(42), AttributeType.NUMERIC),
27+
(bool_(True), AttributeType.BOOLEAN),
28+
(None, AttributeType.OBJECT),
29+
],
30+
)
31+
def test_python_object_to_igraph_attribute_type(input, expected):
32+
assert python_object_to_igraph_attribute_type(input) == expected
33+
34+
35+
@mark.parametrize(
36+
("input", "expected"),
37+
[
38+
(int, AttributeType.NUMERIC),
39+
(float, AttributeType.NUMERIC),
40+
(bool, AttributeType.BOOLEAN),
41+
(str, AttributeType.STRING),
42+
(object, AttributeType.OBJECT),
43+
(int_, AttributeType.NUMERIC),
44+
(bool_, AttributeType.BOOLEAN),
45+
(object_, AttributeType.OBJECT),
46+
],
47+
)
48+
def test_python_type_to_igraph_attribute_type(input, expected):
49+
assert python_type_to_igraph_attribute_type(input) == expected
50+
51+
1652
@mark.parametrize(
1753
("input", "expected"),
1854
[
@@ -32,7 +68,7 @@
3268
],
3369
)
3470
def test_conversion_from_python_to_numpy_array_type(input, expected):
35-
assert get_numpy_attribute_type_from_iterable(input) == expected
71+
assert iterable_to_numpy_attribute_type(input) == expected
3672

3773

3874
@mark.parametrize(
@@ -54,4 +90,4 @@ def test_conversion_from_python_to_numpy_array_type(input, expected):
5490
],
5591
)
5692
def test_conversion_from_python_to_igraph_attribute_type(input, expected):
57-
assert get_igraph_attribute_type_from_iterable(input) == expected
93+
assert iterable_to_igraph_attribute_type(input) == expected

tests/test_attribute_value_list.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,3 +396,21 @@ def test_type_getter():
396396

397397
items = AVL(["foo", "bar", False, 42])
398398
assert items.type == AttributeType.OBJECT
399+
400+
401+
def test_casting():
402+
items = AVL([0, 1, 2, 0, 4])
403+
items.cast(bool)
404+
405+
assert items.type == AttributeType.BOOLEAN
406+
assert list(items) == [False, True, True, False, True]
407+
408+
items.cast(str)
409+
410+
assert items.type == AttributeType.STRING
411+
assert list(items) == ["False", "True", "True", "False", "True"]
412+
413+
items.cast(object)
414+
415+
assert items.type == AttributeType.OBJECT
416+
assert list(items) == ["False", "True", "True", "False", "True"]

tests/test_conversion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from numpy import array
44

55
from igraph_ctypes.constructors import create_empty_graph
6+
from igraph_ctypes.enums import EdgeSequenceType, VertexSequenceType
67
from igraph_ctypes._internal.conversion import (
78
any_to_igraph_bool_t,
89
edgelike_to_igraph_integer_t,
@@ -30,7 +31,6 @@
3031
vertexlike_to_igraph_integer_t,
3132
vertex_selector_to_igraph_vs_t,
3233
)
33-
from igraph_ctypes._internal.enums import EdgeSequenceType, VertexSequenceType
3434
from igraph_ctypes._internal.types import igraph_bool_t, igraph_integer_t
3535
from igraph_ctypes._internal.wrappers import (
3636
_Matrix,

tests/test_write_graph.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ def simple_graph():
1111
return g
1212

1313

14+
@fixture
15+
def graph_with_attributes():
16+
g = create_graph_from_edge_list(
17+
[0, 1, 0, 2, 2, 3, 3, 4, 4, 2, 2, 5, 5, 0, 6, 3, 5, 6], directed=False
18+
)
19+
g.attrs["date"] = "2009-01-10"
20+
return g
21+
22+
1423
def test_write_graph_edgelist(simple_graph, tmp_path, datadir):
1524
path: Path = tmp_path / "edges.txt"
1625
expected = (datadir / "simple_graph.txt").read_text()

0 commit comments

Comments
 (0)