@@ -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