Skip to content

Commit 3b203d9

Browse files
authored
Merge pull request #31 from petercorke/feat/graph-vertex-edge-class
feat: UGraph/DGraph take vertex_class and edge_class
2 parents 53890d1 + fbb258d commit 3b203d9

2 files changed

Lines changed: 304 additions & 21 deletions

File tree

‎src/pgraph/PGraph.py‎

Lines changed: 133 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ def __init__(
2929
heuristic: Callable[[NDArray], float] | str | None = None,
3030
verbose: bool = False,
3131
dim: int | None = None,
32+
vertex_class: type[BaseVertex] | None = None,
33+
edge_class: type[Edge] | None = None,
3234
):
3335
"""
3436
Abstract base class for graphs
@@ -44,15 +46,53 @@ def __init__(
4446
None (unconstrained -- vertices may have coordinates of any
4547
length, or none at all)
4648
:type dim: int, optional
49+
:param vertex_class: class of the vertices this graph creates, defaults
50+
to ``UVertex`` for a :class:`UGraph` and ``DVertex`` for a
51+
:class:`DGraph`
52+
:type vertex_class: subclass of ``UVertex`` or ``DVertex``, optional
53+
:param edge_class: class of the edges this graph creates, defaults to
54+
:class:`Edge`
55+
:type edge_class: subclass of ``Edge``, optional
4756
:raises ValueError: ``dim`` is given but is not a positive integer
57+
:raises TypeError: ``vertex_class`` is not a subclass of this graph's
58+
own vertex kind, or ``edge_class`` is not a subclass of ``Edge``
4859
4960
This is the common base class of :class:`UGraph` and :class:`DGraph`
5061
and should not be instantiated directly.
5162
63+
``vertex_class`` and ``edge_class`` let user subclasses be built by the
64+
graph, so they never need to be constructed by hand::
65+
66+
g = UGraph(vertex_class=MyVertex, edge_class=MyEdge)
67+
v = g.add_vertex([0, 0]) # a MyVertex
68+
e = g.add_edge(v, w) # a MyEdge
69+
70+
The classes are called as ``vertex_class(coord, name=name)`` and
71+
``edge_class(v1, v2, cost=cost, data=data)``, so a subclass that
72+
overrides ``__init__`` must accept and pass on those arguments.
73+
A graph given a ``vertex_class`` only accepts vertices of that class
74+
in :meth:`add_vertex`, and one given an ``edge_class`` only accepts
75+
edges of that class.
76+
5277
:seealso: :class:`UGraph` :class:`DGraph` :meth:`add_vertex`
78+
:meth:`add_edge` :attr:`vertex_class` :attr:`edge_class`
5379
"""
5480
if dim is not None and dim <= 0:
5581
raise ValueError(f"dim must be a positive integer, got {dim!r}")
82+
kind = type(self)._vertex_cls
83+
if vertex_class is None:
84+
vertex_class = kind
85+
elif not (isinstance(vertex_class, type) and issubclass(vertex_class, kind)):
86+
raise TypeError(
87+
f"vertex_class must be a subclass of {kind.__name__} for a "
88+
f"{type(self).__name__}, got {vertex_class!r}"
89+
)
90+
if edge_class is None:
91+
edge_class = Edge
92+
elif not (isinstance(edge_class, type) and issubclass(edge_class, Edge)):
93+
raise TypeError(f"edge_class must be a subclass of Edge, got {edge_class!r}")
94+
self._vertex_class: type[BaseVertex] = vertex_class
95+
self._edge_class: type[Edge] = edge_class
5696
# we use a list and a dict, the list respects the order of adding
5797
self._vertexlist: list[BaseVertex] = []
5898
self._vertexdict: dict[str, BaseVertex] = {}
@@ -100,13 +140,17 @@ def __repr__(self) -> str:
100140
return str(self)
101141

102142
@classmethod
103-
def Dict(cls, d: dict, reverse: bool = False) -> _BaseGraph:
143+
def Dict(cls, d: dict, reverse: bool = False, **kwargs: Any) -> _BaseGraph:
104144
"""
105145
Create graph from parent/child dictionary
106146
107147
:param d: dictionary that maps from ``BaseVertex`` subclass to ``BaseVertex`` subclass
108148
:type d: dict
109149
:param reverse: reverse link direction, defaults to False
150+
:param kwargs: optional arguments passed to the graph constructor, for
151+
example ``vertex_class`` or ``edge_class``
152+
:raises TypeError: ``vertex_class`` or ``edge_class`` is not a
153+
suitable class, see :meth:`_BaseGraph.__init__`
110154
:return: graph
111155
:rtype: UGraph or DGraph
112156
@@ -127,7 +171,7 @@ def Dict(cls, d: dict, reverse: bool = False) -> _BaseGraph:
127171
:seealso: :meth:`Adjacency`
128172
"""
129173

130-
g = cls()
174+
g = cls(**kwargs)
131175

132176
for vertex, parent in d.items():
133177
if isinstance(vertex, str):
@@ -162,6 +206,7 @@ def Adjacency(
162206
A: NDArray,
163207
coords: NDArray | None = None,
164208
names: list[str] | None = None,
209+
**kwargs: Any,
165210
) -> _BaseGraph:
166211
"""
167212
Create graph from adjacency matrix
@@ -172,14 +217,25 @@ def Adjacency(
172217
:type coords: ndarray(N,M), optional
173218
:param names: names of vertices, defaults to None
174219
:type names: list(N) of str, optional
220+
:param kwargs: optional arguments passed to the graph constructor, for
221+
example ``vertex_class`` or ``edge_class``
222+
:raises ValueError: ``A`` is not square, the length of ``names`` or
223+
the number of rows of ``coords`` does not match ``A``, or ``A``
224+
has a non-zero diagonal element and the graph is a ``DGraph``
225+
:raises TypeError: ``vertex_class`` or ``edge_class`` is not a
226+
suitable class, see :meth:`_BaseGraph.__init__`
175227
:return: graph
176228
:rtype: UGraph or DGraph
177229
178230
Create a directed or undirected graph where non-zero elements ``A[i,j]``
179231
correspond to edges from vertex ``i`` to vertex ``j``.
180232
181233
.. warning:: For undirected graph ``A`` should be symmetric but this
182-
is not checked. Only the upper triangular part is used.
234+
is not checked. Only the strictly upper triangular part is used,
235+
so the diagonal is ignored and no self-loops are created. A
236+
directed graph rejects a non-zero diagonal with ``ValueError``.
237+
Edge counts greater than one are not turned into parallel edges;
238+
a non-zero element is a single edge with that element as its cost.
183239
184240
.. runblock:: pycon
185241
@@ -199,7 +255,7 @@ def Adjacency(
199255
if coords is not None and coords.shape[0] != A.shape[0]:
200256
raise ValueError("coords must have same number of rows as adjacency matrix")
201257

202-
g = cls()
258+
g = cls(**kwargs)
203259

204260
name = None
205261
coord = None
@@ -227,6 +283,30 @@ def Adjacency(
227283

228284
return g
229285

286+
@property
287+
def vertex_class(self) -> type[BaseVertex]:
288+
"""
289+
Class of the vertices this graph creates
290+
291+
:return: vertex class
292+
:rtype: subclass of ``UVertex`` or ``DVertex``
293+
294+
:seealso: :meth:`add_vertex` :attr:`edge_class`
295+
"""
296+
return self._vertex_class
297+
298+
@property
299+
def edge_class(self) -> type[Edge]:
300+
"""
301+
Class of the edges this graph creates
302+
303+
:return: edge class
304+
:rtype: subclass of ``Edge``
305+
306+
:seealso: :meth:`add_edge` :attr:`vertex_class`
307+
"""
308+
return self._edge_class
309+
230310
def copy(self) -> _BaseGraph:
231311
"""
232312
Deepcopy of graph
@@ -257,26 +337,29 @@ def add_vertex(
257337
:type coord: array-like or BaseVertex subclass, optional
258338
:param name: name of vertex, defaults to "#i"
259339
:type name: str, optional
260-
:raises TypeError: ``coord`` is a ``BaseVertex`` of the wrong kind
340+
:raises TypeError: ``coord`` is a ``BaseVertex`` that is not an
341+
instance of this graph's :attr:`vertex_class`
261342
:raises ValueError: the graph was constructed with ``dim``, and
262343
``coord`` is given but its length doesn't match
263344
:return: the added vertex
264345
:rtype: BaseVertex subclass
265346
266347
- ``g.add_vertex()`` creates a new vertex with optional ``coord`` and
267348
``name``.
268-
- ``g.add_vertex(v)`` takes an instance or subclass of this graph's
269-
own vertex kind and adds it to the graph.
349+
- ``g.add_vertex(v)`` takes an instance of this graph's
350+
:attr:`vertex_class` (by default any ``UVertex`` for a
351+
:class:`UGraph`, any ``DVertex`` for a :class:`DGraph`) and adds it
352+
to the graph.
270353
271354
If the vertex has no name and ``name`` is None give it a default name
272355
``#N`` where ``N`` is a consecutive integer.
273356
274357
The vertex is placed into a dictionary with a key equal to its name.
275358
276359
This single implementation, shared by :class:`UGraph` and
277-
:class:`DGraph`, is parameterized by each subclass's
278-
:attr:`_vertex_cls` rather than duplicated per subclass -- see
279-
:doc:`policy` for why that matters.
360+
:class:`DGraph`, is parameterized by the graph's :attr:`vertex_class`
361+
rather than duplicated per subclass -- see :doc:`policy` for why that
362+
matters.
280363
281364
.. runblock:: pycon
282365
@@ -301,15 +384,15 @@ def add_vertex(
301384
302385
:seealso: :meth:`vertex_copy`
303386
"""
304-
if isinstance(coord, self._vertex_cls):
387+
if isinstance(coord, self._vertex_class):
305388
vertex = coord
306389
elif isinstance(coord, BaseVertex):
307390
raise TypeError(
308-
f"expecting {self._vertex_cls.__name__} or coordinate data, "
391+
f"expecting {self._vertex_class.__name__} or coordinate data, "
309392
f"got {type(coord).__name__}"
310393
)
311394
else:
312-
vertex = self._vertex_cls(coord, name=name)
395+
vertex = self._vertex_class(coord, name=name)
313396

314397
if (
315398
self._dim is not None
@@ -2297,6 +2380,11 @@ class UGraph(_BaseGraph):
22972380
"""
22982381
Class for undirected graphs
22992382
2383+
Vertices are :class:`UVertex` objects, or instances of the subclass given
2384+
as ``vertex_class``. Edges are :class:`Edge` objects, or instances of the
2385+
subclass given as ``edge_class``; see :class:`_BaseGraph` for the
2386+
constructor arguments and the exceptions they raise.
2387+
23002388
.. inheritance-diagram:: UGraph
23012389
23022390
:seealso: :class:`_BaseGraph` :class:`DGraph`
@@ -2351,6 +2439,11 @@ class DGraph(_BaseGraph):
23512439
"""
23522440
Class for directed graphs
23532441
2442+
Vertices are :class:`DVertex` objects, or instances of the subclass given
2443+
as ``vertex_class``. Edges are :class:`Edge` objects, or instances of the
2444+
subclass given as ``edge_class``; see :class:`_BaseGraph` for the
2445+
constructor arguments and the exceptions they raise.
2446+
23542447
.. inheritance-diagram:: DGraph
23552448
23562449
:seealso: :class:`_BaseGraph` :class:`UGraph`
@@ -2451,6 +2544,9 @@ class Edge:
24512544
edgelist of _each_ vertex.
24522545
- This class can be inherited to provide user objects with graph capability.
24532546
- Inheritance is an alternative to providing arbitrary user data.
2547+
- Pass the subclass as ``edge_class`` when constructing the graph and
2548+
the graph will create instances of it itself, see
2549+
:class:`_BaseGraph`.
24542550
24552551
An Edge points to a pair of vertices. At ``connect`` time the vertices
24562552
get references back to the Edge object.
@@ -2571,6 +2667,8 @@ def connect(self, v1: BaseVertex, v2: BaseVertex) -> None:
25712667
:type v2: BaseVertex subclass
25722668
:raises ValueError: either vertex has not been added to a graph, or
25732669
the vertices belong to different graphs
2670+
:raises TypeError: this edge is not an instance of the graph's
2671+
:attr:`_BaseGraph.edge_class`
25742672
25752673
The edge connects vertices ``v1`` and ``v2``, and is added to the
25762674
graph that those vertices belong to.
@@ -2953,9 +3051,10 @@ def connect(
29533051
29543052
:param dest: The vertex to connect to
29553053
:type dest: ``BaseVertex`` subclass
2956-
:param edge: Use this as the edge object, otherwise a new ``Edge``
2957-
object is created from the vertices being connected,
2958-
and the ``cost`` and ``edge`` parameters, defaults to None
3054+
:param edge: Use this as the edge object, otherwise a new object of the
3055+
graph's :attr:`_BaseGraph.edge_class` is created from the
3056+
vertices being connected, and the ``cost`` and ``data``
3057+
parameters, defaults to None
29593058
:type edge: ``Edge`` subclass, optional
29603059
:param cost: the cost to traverse this edge, defaults to None
29613060
:type cost: float, optional
@@ -2965,6 +3064,8 @@ def connect(
29653064
:raises ValueError: either vertex has not been added to a graph, the
29663065
vertices belong to different graphs, or ``edge`` already connects
29673066
other vertices
3067+
:raises TypeError: ``edge`` is not an instance of the graph's
3068+
:attr:`_BaseGraph.edge_class`
29683069
:return: the edge connecting the vertices
29693070
:rtype: Edge
29703071
@@ -3002,10 +3103,15 @@ def connect(
30023103
elif self._graph is not dest._graph:
30033104
raise ValueError("vertices must belong to the same graph")
30043105
elif isinstance(edge, Edge):
3106+
if not isinstance(edge, self._graph._edge_class):
3107+
raise TypeError(
3108+
f"expecting an edge of class {self._graph._edge_class.__name__}, "
3109+
f"got {type(edge).__name__}"
3110+
)
30053111
e = edge
30063112
self._bind_edge(e, dest)
30073113
else:
3008-
e = Edge(self, dest, cost=cost, data=data)
3114+
e = self._graph._edge_class(self, dest, cost=cost, data=data)
30093115

30103116
self._graph._edgelist.add(e)
30113117
self._graph._connectivitychange = True
@@ -3336,8 +3442,9 @@ def connect(
33363442
33373443
:param dest: vertex to connect to
33383444
:type dest: BaseVertex subclass
3339-
:param edge: Use this as the edge object, otherwise a new ``Edge``
3340-
object is created, defaults to None
3445+
:param edge: Use this as the edge object, otherwise a new object of the
3446+
graph's :attr:`_BaseGraph.edge_class` is created,
3447+
defaults to None
33413448
:type edge: ``Edge`` subclass, optional
33423449
:param cost: the cost to traverse this edge, defaults to None
33433450
:type cost: float, optional
@@ -3347,6 +3454,8 @@ def connect(
33473454
:raises ValueError: either vertex has not been added to a graph, the
33483455
vertices belong to different graphs, or ``edge`` already connects
33493456
other vertices
3457+
:raises TypeError: ``edge`` is not an instance of the graph's
3458+
:attr:`_BaseGraph.edge_class`
33503459
:return: the edge connecting the vertices
33513460
:rtype: Edge
33523461
@@ -3412,8 +3521,9 @@ def connect(
34123521
34133522
:param dest: vertex to connect to
34143523
:type dest: BaseVertex subclass
3415-
:param edge: Use this as the edge object, otherwise a new ``Edge``
3416-
object is created, defaults to None
3524+
:param edge: Use this as the edge object, otherwise a new object of the
3525+
graph's :attr:`_BaseGraph.edge_class` is created,
3526+
defaults to None
34173527
:type edge: ``Edge`` subclass, optional
34183528
:param cost: the cost to traverse this edge, defaults to None
34193529
:type cost: float, optional
@@ -3423,6 +3533,8 @@ def connect(
34233533
:raises ValueError: either vertex has not been added to a graph, the
34243534
vertices belong to different graphs, or ``edge`` already connects
34253535
other vertices
3536+
:raises TypeError: ``edge`` is not an instance of the graph's
3537+
:attr:`_BaseGraph.edge_class`
34263538
:return: the edge connecting the vertices
34273539
:rtype: Edge
34283540

0 commit comments

Comments
 (0)