@@ -323,15 +323,38 @@ def __str__(self) -> str:
323323 >>> g.add_edge(v2, v3)
324324 >>> str(g)
325325
326- :seealso: :meth:`show`
326+ :seealso: :meth:`__repr__` :meth:` show`
327327 """
328328 s = f"{ self .__class__ .__name__ } : { self .n :,} { 'vertex' if self .n == 1 else 'vertices' } , { self .ne :,} edge{ 's' [:self .ne ^ 1 ]} , { self .nc :,} component{ 's' [:self .nc ^ 1 ]} "
329329 return s
330330
331331 def __repr__ (self ) -> str :
332- # NOTE: this is shadowed by the __repr__ defined further below in
333- # this class, which is the one actually used -- kept as-is here to
334- # avoid changing behaviour as part of a typing-only pass.
332+ """
333+ Representation of the graph, the same one-line summary as :meth:`__str__`
334+
335+ :return: one-line summary of vertex/edge/component counts
336+ :rtype: str
337+
338+ The representation of a graph does not list its vertices, since that
339+ would be as long as the graph, and a ``repr`` is what appears in a
340+ notebook cell, a debugger, a log or an error message. Use
341+ :meth:`show` to list them.
342+
343+ .. runblock:: pycon
344+
345+ >>> from pgraph import UGraph
346+ >>> g = UGraph()
347+ >>> v1 = g.add_vertex(coord=[0,0], name='v1')
348+ >>> v2 = g.add_vertex(coord=[1,1], name='v2')
349+ >>> g.add_edge(v1, v2)
350+ >>> repr(g)
351+
352+ .. versionchanged:: 1.1
353+ Was one line per vertex, with its name, coordinate and component;
354+ that is now printed by :meth:`show`.
355+
356+ :seealso: :meth:`__str__` :meth:`show`
357+ """
335358 return str (self )
336359
337360 @classmethod
@@ -1459,14 +1482,14 @@ def remove_vertex(self, vertex: Vertex) -> None:
14591482 """
14601483 if vertex ._graph is not self :
14611484 raise ValueError ("vertex does not belong to this graph" )
1462- assert vertex . name is not None
1485+ name = vertex . _require_name ()
14631486
14641487 # its own lists name every edge at it, so no need to search the graph
14651488 for edge in vertex ._incident_edges ():
14661489 self .remove_edge (edge )
14671490
14681491 self ._vertexlist .remove (vertex )
1469- del self ._vertexdict [vertex . name ]
1492+ del self ._vertexdict [name ]
14701493 vertex ._graph = None # it is not in any graph now
14711494 vertex ._label = None # and so not in a component
14721495 self ._connectivitychange = True
@@ -1501,7 +1524,11 @@ def remove(self, x: Edge | Vertex) -> None:
15011524
15021525 def show (self ) -> None :
15031526 """
1504- Print a summary of all vertices and edges to stdout
1527+ Print a listing of all vertices and edges to stdout
1528+
1529+ Each vertex is shown with its name, coordinate and component number,
1530+ and each edge by its endpoints, cost and data. This is as long as the
1531+ graph, unlike :meth:`__str__` and :meth:`__repr__`, which are one line.
15051532
15061533 .. runblock:: pycon
15071534
@@ -1512,11 +1539,18 @@ def show(self) -> None:
15121539 >>> g.add_edge(v1, v2)
15131540 >>> g.show()
15141541
1515- :seealso: :meth:`__str__`
1542+ .. versionchanged:: 1.1
1543+ Each vertex is shown with its coordinate and component number,
1544+ which were what ``repr(g)`` listed before it became one line.
1545+
1546+ :seealso: :meth:`__str__` :meth:`plot`
15161547 """
15171548 print ("vertices:" )
15181549 for v in self ._vertexlist :
1519- print (" " + str (v ))
1550+ line = f" { v .name } at { v .coord } "
1551+ if v .label is not None :
1552+ line += f" component={ v .label } "
1553+ print (line )
15201554 print ("edges:" )
15211555 for e in self ._edges :
15221556 print (" " + str (e ))
@@ -1808,33 +1842,6 @@ def heuristic(self, heuristic: Callable[[NDArray], float] | str) -> None:
18081842 """
18091843 self ._heuristic = self ._metricfunc (heuristic )
18101844
1811- def __repr__ (self ) -> str : # type: ignore[no-redef]
1812- """
1813- Detailed representation of the graph, one line per vertex
1814-
1815- :return: one line per vertex showing its name, coordinate and component
1816- :rtype: str
1817-
1818- .. runblock:: pycon
1819-
1820- >>> from pgraph import UGraph
1821- >>> g = UGraph()
1822- >>> v1 = g.add_vertex(coord=[0,0], name='v1')
1823- >>> v2 = g.add_vertex(coord=[1,1], name='v2')
1824- >>> v3 = g.add_vertex(coord=[2,2], name='v3')
1825- >>> g.add_edge(v1, v2)
1826- >>> g.add_edge(v2, v3)
1827- >>> repr(g)
1828-
1829- """
1830- s = [f"{ self .__class__ .__name__ } :" ]
1831- for vertex in self :
1832- ss = f" { vertex .name } at { vertex .coord } "
1833- if vertex .label is not None :
1834- ss += f" component={ vertex .label } "
1835- s .append (ss )
1836- return "\n " .join (s )
1837-
18381845 def __getitem__ (self , i : int | str | Vertex ) -> Vertex :
18391846 """
18401847 Get vertex (base class method)
@@ -2266,7 +2273,8 @@ def near(self, coord: ArrayLike, radius: float) -> list[tuple[Vertex, float]]:
22662273
22672274 found = []
22682275 for vertex in candidates :
2269- assert vertex .coord is not None # the candidates all have a position
2276+ # type narrowing only: the candidates all have a position
2277+ assert vertex .coord is not None # nosec B101
22702278 dist = float (self .metric (vertex .coord - coord ))
22712279 if dist <= radius :
22722280 found .append ((vertex , dist ))
@@ -2332,19 +2340,19 @@ def _edge_layout(self) -> tuple[dict[Edge, float], dict[Edge, int]]:
23322340 groups : dict [frozenset [int ], list [Edge ]] = {}
23332341 loops : dict [Vertex , list [Edge ]] = {}
23342342 for e in self ._edges :
2335- assert e . v1 is not None and e . v2 is not None
2336- if e . v1 is e . v2 :
2337- loops .setdefault (e . v1 , []).append (e )
2343+ v1 , v2 = e . _ends ()
2344+ if v1 is v2 :
2345+ loops .setdefault (v1 , []).append (e )
23382346 else :
2339- groups .setdefault (frozenset ((index [e . v1 ], index [e . v2 ])), []).append (e )
2347+ groups .setdefault (frozenset ((index [v1 ], index [v2 ])), []).append (e )
23402348 rads : dict [Edge , float ] = {}
23412349 for edges in groups .values ():
23422350 n = len (edges )
23432351 for k , e in enumerate (edges ):
2344- assert e . v1 is not None and e . v2 is not None
2352+ v1 , v2 = e . _ends ()
23452353 offset = (k - (n - 1 ) / 2 ) * _ARC_STEP
23462354 # the same side in space for an edge drawn the other way round
2347- rads [e ] = offset if index [e . v1 ] < index [e . v2 ] else - offset
2355+ rads [e ] = offset if index [v1 ] < index [v2 ] else - offset
23482356 loopno = {e : k for edges in loops .values () for k , e in enumerate (edges )}
23492357 return rads , loopno
23502358
@@ -2373,8 +2381,7 @@ def _edge_artist(
23732381 from matplotlib .patches import FancyArrowPatch
23742382 from matplotlib .transforms import Affine2D , ScaledTranslation
23752383 from ._plot import _Arrow3D , _Loop3D , _loop_path
2376- v1 , v2 = edge .v1 , edge .v2
2377- assert v1 is not None and v2 is not None
2384+ v1 , v2 = edge ._ends ()
23782385 shrink = markersize / 2 + 1
23792386 if v1 is v2 :
23802387 radius = _LOOP_RADIUS + _LOOP_GROWTH * loopno
@@ -2528,7 +2535,7 @@ def plot(
25282535 ... g.add_edge(a, b);
25292536 >>> g.plot(block=None)
25302537
2531- :seealso: :meth:`highlight_path` :meth:`highlight_edge`
2538+ :seealso: :meth:`highlight_path` :meth:`highlight_edge` :meth:`show`
25322539 """
25332540 import matplotlib .pyplot as plt
25342541 from matplotlib .patches import FancyArrowPatch
@@ -2620,10 +2627,10 @@ def plot(
26202627 # the edges, each once
26212628 rads , loopno = self ._edge_layout ()
26222629 for e in self ._edges :
2623- assert e . v1 is not None
2630+ start = e . _ends ()[ 0 ]
26242631 kwargs = dict (edge_kwargs )
26252632 if color_components :
2626- kwargs = {** dict (color = color [e . v1 .label , :]), ** kwargs }
2633+ kwargs = {** dict (color = color [start .label , :]), ** kwargs }
26272634 artist = self ._edge_artist (
26282635 ax ,
26292636 e ,
@@ -2649,10 +2656,8 @@ def plot(
26492656 )
26502657 dx , dy = abs (x1 - x0 ), abs (y1 - y0 )
26512658 for e in loopno :
2652- assert e .v1 is not None
2653- ax .update_datalim (
2654- [(e .v1 .x - dx , e .v1 .y - dy ), (e .v1 .x + dx , e .v1 .y + dy )]
2655- )
2659+ v = e ._ends ()[0 ]
2660+ ax .update_datalim ([(v .x - dx , v .y - dy ), (v .x + dx , v .y + dy )])
26562661 ax .autoscale_view ()
26572662
26582663 ax .grid (grid )
@@ -3436,11 +3441,11 @@ def incidence(self) -> NDArray:
34363441 vdict [vertex ] = i
34373442
34383443 for j , e in enumerate (edges ):
3439- assert e . v1 is not None and e . v2 is not None
3440- I [vdict [e . v1 ], j ] = 1
3441- I [vdict [e . v2 ], j ] = 1
3442- if e . v1 is e . v2 and not self .directed :
3443- I [vdict [e . v1 ], j ] = 2 # self-loop, counted at both ends
3444+ v1 , v2 = e . _ends ()
3445+ I [vdict [v1 ], j ] = 1
3446+ I [vdict [v2 ], j ] = 1
3447+ if v1 is v2 and not self .directed :
3448+ I [vdict [v1 ], j ] = 2 # self-loop, counted at both ends
34443449
34453450 return I
34463451
@@ -3793,8 +3798,7 @@ def path_UCS(
37933798
37943799 parent_names : dict [str , str ] = {}
37953800 for v , p in parent .items ():
3796- assert v .name is not None and p .name is not None
3797- parent_names [v .name ] = p .name
3801+ parent_names [v ._require_name ()] = p ._require_name ()
37983802
37993803 if summary or verbose :
38003804 print (
@@ -3931,8 +3935,7 @@ def path_Astar(
39313935
39323936 parent_names : dict [str , str ] = {}
39333937 for v , p in parent .items ():
3934- assert v .name is not None and p .name is not None
3935- parent_names [v .name ] = p .name
3938+ parent_names [v ._require_name ()] = p ._require_name ()
39363939
39373940 if summary or verbose :
39383941 print (
@@ -3973,9 +3976,9 @@ def _attach(self, edge: Edge) -> None:
39733976 from either end. A self-loop is therefore held twice by its vertex,
39743977 which is why it counts 2 towards the degree.
39753978 """
3976- assert edge . v1 is not None and edge .v2 is not None
3977- edge . v1 ._edgelist .append (edge )
3978- edge . v2 ._edgelist .append (edge )
3979+ v1 , v2 = edge ._ends ()
3980+ v1 ._edgelist .append (edge )
3981+ v2 ._edgelist .append (edge )
39793982
39803983 def _detach (self , edge : Edge ) -> None :
39813984 """
@@ -4028,9 +4031,9 @@ def _attach(self, edge: Edge) -> None:
40284031 The edge goes in the start vertex's list of outgoing edges and in the
40294032 end vertex's list of incoming ones.
40304033 """
4031- assert edge . v1 is not None and edge .v2 is not None
4032- edge . v1 ._edgelist .append (edge )
4033- edge . v2 ._inlist .append (edge )
4034+ v1 , v2 = edge ._ends ()
4035+ v1 ._edgelist .append (edge )
4036+ v2 ._inlist .append (edge )
40344037
40354038 def _detach (self , edge : Edge ) -> None :
40364039 """
@@ -4203,6 +4206,27 @@ def _set_endpoints(self, v1: Vertex | None, v2: Vertex | None) -> None:
42034206 self ._v1 = v1
42044207 self ._v2 = v2
42054208
4209+ def _ends (self ) -> tuple [Vertex , Vertex ]:
4210+ """
4211+ Both vertices of the edge, raising clearly if it is not connected (private method)
4212+
4213+ :raises ValueError: the edge has no vertices
4214+ :return: the first and second vertex
4215+ :rtype: tuple of Vertex subclass
4216+
4217+ ``v1`` and ``v2`` are None for an edge that has not been connected.
4218+ Every edge in a graph has both, so the code that works on them takes
4219+ them from here, which tells the type checker so, rather than testing
4220+ for None at each use. Unlike an ``assert`` this check is not removed
4221+ by ``python -O``.
4222+
4223+ :seealso: :attr:`v1` :attr:`v2`
4224+ """
4225+ v1 , v2 = self ._v1 , self ._v2
4226+ if v1 is None or v2 is None :
4227+ raise ValueError ("the edge is not connected to two vertices" )
4228+ return v1 , v2
4229+
42064230 def __reduce__ (self ) -> Any :
42074231 """
42084232 Refuse to pickle or copy an edge on its own
@@ -4334,12 +4358,11 @@ def next(self, vertex: Vertex) -> Vertex:
43344358
43354359 """
43364360
4337- if self .v1 is vertex :
4338- assert self .v2 is not None
4339- return self .v2
4340- elif self .v2 is vertex :
4341- assert self .v1 is not None
4342- return self .v1
4361+ v1 , v2 = self ._ends ()
4362+ if v1 is vertex :
4363+ return v2
4364+ elif v2 is vertex :
4365+ return v1
43434366 else :
43444367 raise ValueError ("should not happen" )
43454368
@@ -4378,8 +4401,7 @@ def endpoints(self) -> list[Vertex]:
43784401
43794402 :seealso: :meth:`vertices`
43804403 """
4381- assert self .v1 is not None and self .v2 is not None
4382- return [self .v1 , self .v2 ]
4404+ return list (self ._ends ())
43834405
43844406 def remove (self ) -> None :
43854407 """
@@ -4804,7 +4826,8 @@ def _bind_edge(self, edge: Edge, dest: Vertex) -> None:
48044826 endpoints disagree with the vertices' edge lists.
48054827 """
48064828 graph = self ._graph
4807- assert graph is not None # connect() has checked
4829+ # type narrowing only: connect() has checked
4830+ assert graph is not None # nosec B101
48084831 if edge .v1 is None and edge .v2 is None :
48094832 edge ._set_endpoints (self , dest )
48104833 elif not graph ._endpoints_match (edge , self , dest ):
@@ -5141,6 +5164,25 @@ def _incident_edges(self) -> list[Edge]:
51415164 seen .setdefault (id (e ), e )
51425165 return list (seen .values ())
51435166
5167+ def _require_name (self ) -> str :
5168+ """
5169+ The name, raising clearly if the vertex has none (private method)
5170+
5171+ :raises ValueError: the vertex has no name
5172+ :return: the name
5173+ :rtype: str
5174+
5175+ A vertex that is not in a graph can have no name, but one in a graph
5176+ always has, so the code that works on the vertices of a graph takes
5177+ the name from here, which tells the type checker so. Unlike an
5178+ ``assert`` this check is not removed by ``python -O``.
5179+
5180+ :seealso: :attr:`name`
5181+ """
5182+ if self ._name is None :
5183+ raise ValueError ("the vertex has no name" )
5184+ return self ._name
5185+
51445186 @property
51455187 def name (self ) -> str | None :
51465188 """
0 commit comments