Skip to content

Commit ddfbb7b

Browse files
committed
feat: first version of attribute maps that seems feasible in the long term
1 parent e0fc0b0 commit ddfbb7b

File tree

9 files changed

+244
-32
lines changed

9 files changed

+244
-32
lines changed

.coveragerc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ omit = src/codegen/*
55
exclude_lines =
66
pragma: no cover
77
if TYPE_CHECKING:
8-
^\s*\.\.\.$
8+
^\s*\.\.\.$
9+
raise NotImplementedError
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from .handler import AttributeHandler
2+
from .map import AttributeMap
23
from .storage import AttributeStorage
34
from .value_list import AttributeValueList
45

56
__all__ = (
67
"AttributeHandler",
8+
"AttributeMap",
79
"AttributeStorage",
810
"AttributeValueList",
911
)

src/igraph_ctypes/_internal/attributes/handler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def add_vertices(self, graph, n: int, attr) -> None:
102102
raise RuntimeError(
103103
"add_vertices() attribute handler called with non-null attr; "
104104
"this is most likely a bug"
105-
)
105+
) # pragma: no cover
106106

107107
# Extend the existing attribute containers
108108
get_storage_from_graph(graph).add_vertices(graph, n)
@@ -119,7 +119,7 @@ def add_edges(self, graph, edges, attr) -> None:
119119
raise RuntimeError(
120120
"add_edges() attribute handler called with non-null attr; "
121121
"this is most likely a bug"
122-
)
122+
) # pragma: no cover
123123

124124
# Extend the existing attribute containers
125125
edge_array = igraph_vector_int_t_to_numpy_array_view(edges).reshape((-1, 2))
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from collections.abc import MutableMapping
2+
from typing import Iterable, Iterator, TypeVar
3+
4+
from .value_list import AttributeValueList
5+
6+
__all__ = ("AttributeMap",)
7+
8+
9+
T = TypeVar("T")
10+
C = TypeVar("C", bound="AttributeMap")
11+
12+
13+
class AttributeMap(MutableMapping[str, AttributeValueList[T]]):
14+
"""Helper object that provides a mutable mapping-like interface to access
15+
the vertex or edge attributes of a graph.
16+
17+
Enforces that values assigned to keys in the map are of type
18+
AttributeValueList_ and that they have the right length.
19+
"""
20+
21+
_items: dict[str, AttributeValueList[T]]
22+
_common_length_of_values: int = 0
23+
24+
@classmethod
25+
def wrap_empty_dict(cls, length: int = 0):
26+
return cls({}, length)
27+
28+
def __init__(self, items: dict[str, AttributeValueList[T]], length: int) -> None:
29+
self._common_length_of_values = length
30+
self._items = items
31+
32+
assert all(v.fixed_length and len(v) == length for v in items.values())
33+
34+
def copy(self: C) -> C:
35+
"""Returns a shallow copy of the attribute map.
36+
37+
Note that unlike with regular dictionaries, this function makes a
38+
shallow copy of the _values_ as well.
39+
"""
40+
return self.__class__(
41+
{k: v.copy() for k, v in self._items.items()},
42+
self._common_length_of_values,
43+
)
44+
45+
def copy_empty(self: C) -> C:
46+
"""Returns another, empty attribute map with the same expected length
47+
for any items being assigned in the future.
48+
"""
49+
return self.__class__.wrap_empty_dict(self._common_length_of_values)
50+
51+
def remove(self, key: str) -> None:
52+
del self._items[key]
53+
54+
def set(self, key: str, value: T | Iterable[T]) -> None:
55+
"""Assigns a value to _all_ the attribute values corresponding to the
56+
given attribute.
57+
58+
This function is also available as the ``__setitem__`` magic method,
59+
making it possible to use the class as if it was a dictionary.
60+
61+
Args:
62+
key: the name of the attribute to set
63+
value: the new value of the attribute. When it is an iterable
64+
that is not a string or a bytes object, it is assumed to
65+
contain the values for all items (vertices or edges)
66+
individually, and its length will be checked against the
67+
number of vertices or edges. WHen it is not an iterable, or
68+
it is a string or bytes object, it is assumed to be a common
69+
value for all vertices or edges.
70+
"""
71+
length = self._common_length_of_values
72+
73+
if isinstance(value, (bytes, str)):
74+
# strings and bytes are iterable but they are treated as if not
75+
avl = AttributeValueList(
76+
[value] * length, fixed_length=True
77+
) # type: ignore
78+
elif isinstance(value, Iterable):
79+
# iterables are mapped to an AttributeValueList. Note that this
80+
# also takes care of copying existing AttributeValueList instances
81+
avl = AttributeValueList(value, fixed_length=True) # type: ignore
82+
if len(avl) != length:
83+
raise RuntimeError(
84+
f"attribute value list length must be {length}, got {len(avl)}"
85+
)
86+
else:
87+
# all other values are assumed to be a common value for all
88+
# vertices or edges
89+
avl = AttributeValueList(
90+
[value] * length, fixed_length=True
91+
) # type: ignore
92+
93+
assert avl.fixed_length and len(avl) == length
94+
self._items[key] = avl
95+
96+
def _extend_common_length(self, n: int) -> None:
97+
self._common_length_of_values += n
98+
for value_list in self._items.values():
99+
value_list._extend_length(n)
100+
101+
def __getitem__(self, key: str) -> AttributeValueList[T]:
102+
return self._items[key]
103+
104+
def __iter__(self) -> Iterator[str]:
105+
return self._items.__iter__()
106+
107+
def __len__(self) -> int:
108+
return len(self._items)
109+
110+
__delitem__ = remove
111+
__setitem__ = set # type: ignore

src/igraph_ctypes/_internal/attributes/storage.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from igraph_ctypes._internal.refcount import incref, decref
77
from igraph_ctypes._internal.types import IntArray
88

9-
from .value_list import AttributeValueList
9+
from .map import AttributeMap
1010

1111
__all__ = (
1212
"AttributeStorage",
@@ -59,16 +59,16 @@ def get_graph_attribute_map(self) -> MutableMapping[str, Any]:
5959
raise NotImplementedError
6060

6161
@abstractmethod
62-
def get_vertex_attribute_map(self) -> MutableMapping[str, AttributeValueList[Any]]:
63-
"""Returns a mutable mapping into the storage area that stores the
64-
vertex attributes.
62+
def get_vertex_attribute_map(self) -> AttributeMap:
63+
"""Returns an attribute map corresponding to the storage area that
64+
stores the vertex attributes.
6565
"""
6666
raise NotImplementedError
6767

6868
@abstractmethod
69-
def get_edge_attribute_map(self) -> MutableMapping[str, AttributeValueList[Any]]:
70-
"""Returns a mutable mapping into the storage area that stores the
71-
edge attributes.
69+
def get_edge_attribute_map(self) -> AttributeMap:
70+
"""Returns an attribute map corresponding to the storage area that
71+
stores the edge attributes.
7272
"""
7373
raise NotImplementedError
7474

@@ -80,17 +80,18 @@ class DictAttributeStorage(AttributeStorage):
8080
"""
8181

8282
graph_attributes: dict[str, Any] = field(default_factory=dict)
83-
vertex_attributes: dict[str, AttributeValueList[Any]] = field(default_factory=dict)
84-
edge_attributes: dict[str, AttributeValueList[Any]] = field(default_factory=dict)
83+
vertex_attributes: AttributeMap[Any] = field(
84+
default_factory=AttributeMap.wrap_empty_dict
85+
)
86+
edge_attributes: AttributeMap[Any] = field(
87+
default_factory=AttributeMap.wrap_empty_dict
88+
)
8589

8690
def add_vertices(self, graph, n: int) -> None:
87-
for value_list in self.vertex_attributes.values():
88-
value_list._extend_length(n)
91+
self.vertex_attributes._extend_common_length(n)
8992

9093
def add_edges(self, graph, edges: IntArray) -> None:
91-
n = edges.shape[0]
92-
for value_list in self.edge_attributes.values():
93-
value_list._extend_length(n)
94+
self.edge_attributes._extend_common_length(edges.shape[0])
9495

9596
def clear(self) -> None:
9697
self.graph_attributes.clear()
@@ -105,21 +106,21 @@ def copy(
105106
):
106107
return self.__class__(
107108
self.graph_attributes.copy() if copy_graph_attributes else {},
108-
{k: v.copy() for k, v in self.vertex_attributes.items()}
109+
self.vertex_attributes.copy()
109110
if copy_vertex_attributes
110-
else {},
111-
{k: v.copy() for k, v in self.edge_attributes.items()}
111+
else self.vertex_attributes.copy_empty(),
112+
self.edge_attributes.copy()
112113
if copy_edge_attributes
113-
else {},
114+
else self.edge_attributes.copy_empty(),
114115
)
115116

116117
def get_graph_attribute_map(self) -> MutableMapping[str, Any]:
117118
return self.graph_attributes
118119

119-
def get_vertex_attribute_map(self) -> MutableMapping[str, AttributeValueList[Any]]:
120+
def get_vertex_attribute_map(self) -> AttributeMap:
120121
return self.vertex_attributes
121122

122-
def get_edge_attribute_map(self) -> MutableMapping[str, AttributeValueList[Any]]:
123+
def get_edge_attribute_map(self) -> AttributeMap:
123124
return self.edge_attributes
124125

125126

src/igraph_ctypes/_internal/attributes/value_list.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
import numpy as np
22

33
from itertools import repeat
4-
from math import ceil, floor
4+
from math import ceil
55
from numpy.typing import NDArray
66
from types import EllipsisType
7-
from typing import Any, cast, Iterable, NoReturn, Sequence, Sized, TypeVar, overload
7+
from typing import (
8+
Any,
9+
cast,
10+
Iterable,
11+
NoReturn,
12+
Sequence,
13+
Sized,
14+
TypeVar,
15+
overload,
16+
)
817

918
__all__ = ("AttributeValueList",)
1019

1120
C = TypeVar("C", bound="AttributeValueList")
12-
T = TypeVar("T")
21+
T = TypeVar("T", covariant=True)
1322

1423

1524
BoolLike = bool | np.bool_
@@ -24,7 +33,7 @@
2433
)
2534

2635

27-
class AttributeValueList(Sequence[T]):
36+
class AttributeValueList(Sequence[T | None]):
2837
"""List-like data structure that stores the values of a vertex or an edge
2938
attribute for every vertex and edge in the graph, while supporting
3039
NumPy-style fancy indexing operations.
@@ -60,7 +69,7 @@ class AttributeValueList(Sequence[T]):
6069
the graph.
6170
"""
6271

63-
_items: list[T]
72+
_items: list[T | None]
6473
"""The items in the list."""
6574

6675
_fixed_length: bool
@@ -130,7 +139,7 @@ def __delitem__(self, index: IntLike) -> None: # noqa: C901
130139
raise RuntimeError("cannot delete items from a fixed-length list")
131140

132141
@overload
133-
def __getitem__(self, index: IntLike) -> T:
142+
def __getitem__(self, index: IntLike) -> T | None:
134143
...
135144

136145
@overload

src/igraph_ctypes/graph.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
VertexSelector,
1313
)
1414

15-
from ._internal.attributes import AttributeStorage, AttributeValueList
15+
from ._internal.attributes import AttributeMap, AttributeStorage
1616
from ._internal.functions import (
1717
add_edges,
1818
add_vertices,
@@ -124,7 +124,7 @@ def attrs(self) -> MutableMapping[str, Any]:
124124
return self._get_attribute_storage().get_graph_attribute_map()
125125

126126
@property
127-
def vattrs(self) -> MutableMapping[str, AttributeValueList]:
127+
def vattrs(self) -> AttributeMap[Any]:
128128
"""Provides access to the user-defined attributes of the vertices of the
129129
graph.
130130
@@ -133,7 +133,7 @@ def vattrs(self) -> MutableMapping[str, AttributeValueList]:
133133
return self._get_attribute_storage().get_vertex_attribute_map()
134134

135135
@property
136-
def eattrs(self) -> MutableMapping[str, AttributeValueList]:
136+
def eattrs(self) -> AttributeMap[Any]:
137137
"""Provides access to the user-defined attributes of the edges of the
138138
graph.
139139

tests/test_attribute_value_list.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,3 +356,11 @@ def test_delitem(items: AVL, to_delete, expected: list[int]):
356356
def test_delitem_invalid_index(items: AVL):
357357
with raises(IndexError, match="valid indices"):
358358
del items[RuntimeError] # type: ignore
359+
360+
361+
def test__extend_length(items: AVL):
362+
assert len(items) == 5 and items.fixed_length
363+
items._extend_length(3)
364+
assert len(items) == 8 and items.fixed_length
365+
items._extend_length(0)
366+
assert len(items) == 8 and items.fixed_length

0 commit comments

Comments
 (0)