@@ -1459,14 +1459,14 @@ def remove_vertex(self, vertex: Vertex) -> None:
14591459 """
14601460 if vertex ._graph is not self :
14611461 raise ValueError ("vertex does not belong to this graph" )
1462- assert vertex . name is not None
1462+ name = vertex . _require_name ()
14631463
14641464 # its own lists name every edge at it, so no need to search the graph
14651465 for edge in vertex ._incident_edges ():
14661466 self .remove_edge (edge )
14671467
14681468 self ._vertexlist .remove (vertex )
1469- del self ._vertexdict [vertex . name ]
1469+ del self ._vertexdict [name ]
14701470 vertex ._graph = None # it is not in any graph now
14711471 vertex ._label = None # and so not in a component
14721472 self ._connectivitychange = True
@@ -2266,7 +2266,8 @@ def near(self, coord: ArrayLike, radius: float) -> list[tuple[Vertex, float]]:
22662266
22672267 found = []
22682268 for vertex in candidates :
2269- assert vertex .coord is not None # the candidates all have a position
2269+ # type narrowing only: the candidates all have a position
2270+ assert vertex .coord is not None # nosec B101
22702271 dist = float (self .metric (vertex .coord - coord ))
22712272 if dist <= radius :
22722273 found .append ((vertex , dist ))
@@ -2332,19 +2333,19 @@ def _edge_layout(self) -> tuple[dict[Edge, float], dict[Edge, int]]:
23322333 groups : dict [frozenset [int ], list [Edge ]] = {}
23332334 loops : dict [Vertex , list [Edge ]] = {}
23342335 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 )
2336+ v1 , v2 = e . _ends ()
2337+ if v1 is v2 :
2338+ loops .setdefault (v1 , []).append (e )
23382339 else :
2339- groups .setdefault (frozenset ((index [e . v1 ], index [e . v2 ])), []).append (e )
2340+ groups .setdefault (frozenset ((index [v1 ], index [v2 ])), []).append (e )
23402341 rads : dict [Edge , float ] = {}
23412342 for edges in groups .values ():
23422343 n = len (edges )
23432344 for k , e in enumerate (edges ):
2344- assert e . v1 is not None and e . v2 is not None
2345+ v1 , v2 = e . _ends ()
23452346 offset = (k - (n - 1 ) / 2 ) * _ARC_STEP
23462347 # 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
2348+ rads [e ] = offset if index [v1 ] < index [v2 ] else - offset
23482349 loopno = {e : k for edges in loops .values () for k , e in enumerate (edges )}
23492350 return rads , loopno
23502351
@@ -2373,8 +2374,7 @@ def _edge_artist(
23732374 from matplotlib .patches import FancyArrowPatch
23742375 from matplotlib .transforms import Affine2D , ScaledTranslation
23752376 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
2377+ v1 , v2 = edge ._ends ()
23782378 shrink = markersize / 2 + 1
23792379 if v1 is v2 :
23802380 radius = _LOOP_RADIUS + _LOOP_GROWTH * loopno
@@ -2620,10 +2620,10 @@ def plot(
26202620 # the edges, each once
26212621 rads , loopno = self ._edge_layout ()
26222622 for e in self ._edges :
2623- assert e . v1 is not None
2623+ start = e . _ends ()[ 0 ]
26242624 kwargs = dict (edge_kwargs )
26252625 if color_components :
2626- kwargs = {** dict (color = color [e . v1 .label , :]), ** kwargs }
2626+ kwargs = {** dict (color = color [start .label , :]), ** kwargs }
26272627 artist = self ._edge_artist (
26282628 ax ,
26292629 e ,
@@ -2649,10 +2649,8 @@ def plot(
26492649 )
26502650 dx , dy = abs (x1 - x0 ), abs (y1 - y0 )
26512651 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- )
2652+ v = e ._ends ()[0 ]
2653+ ax .update_datalim ([(v .x - dx , v .y - dy ), (v .x + dx , v .y + dy )])
26562654 ax .autoscale_view ()
26572655
26582656 ax .grid (grid )
@@ -3436,11 +3434,11 @@ def incidence(self) -> NDArray:
34363434 vdict [vertex ] = i
34373435
34383436 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
3437+ v1 , v2 = e . _ends ()
3438+ I [vdict [v1 ], j ] = 1
3439+ I [vdict [v2 ], j ] = 1
3440+ if v1 is v2 and not self .directed :
3441+ I [vdict [v1 ], j ] = 2 # self-loop, counted at both ends
34443442
34453443 return I
34463444
@@ -3793,8 +3791,7 @@ def path_UCS(
37933791
37943792 parent_names : dict [str , str ] = {}
37953793 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
3794+ parent_names [v ._require_name ()] = p ._require_name ()
37983795
37993796 if summary or verbose :
38003797 print (
@@ -3931,8 +3928,7 @@ def path_Astar(
39313928
39323929 parent_names : dict [str , str ] = {}
39333930 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
3931+ parent_names [v ._require_name ()] = p ._require_name ()
39363932
39373933 if summary or verbose :
39383934 print (
@@ -3973,9 +3969,9 @@ def _attach(self, edge: Edge) -> None:
39733969 from either end. A self-loop is therefore held twice by its vertex,
39743970 which is why it counts 2 towards the degree.
39753971 """
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 )
3972+ v1 , v2 = edge ._ends ()
3973+ v1 ._edgelist .append (edge )
3974+ v2 ._edgelist .append (edge )
39793975
39803976 def _detach (self , edge : Edge ) -> None :
39813977 """
@@ -4028,9 +4024,9 @@ def _attach(self, edge: Edge) -> None:
40284024 The edge goes in the start vertex's list of outgoing edges and in the
40294025 end vertex's list of incoming ones.
40304026 """
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 )
4027+ v1 , v2 = edge ._ends ()
4028+ v1 ._edgelist .append (edge )
4029+ v2 ._inlist .append (edge )
40344030
40354031 def _detach (self , edge : Edge ) -> None :
40364032 """
@@ -4203,6 +4199,27 @@ def _set_endpoints(self, v1: Vertex | None, v2: Vertex | None) -> None:
42034199 self ._v1 = v1
42044200 self ._v2 = v2
42054201
4202+ def _ends (self ) -> tuple [Vertex , Vertex ]:
4203+ """
4204+ Both vertices of the edge, raising clearly if it is not connected (private method)
4205+
4206+ :raises ValueError: the edge has no vertices
4207+ :return: the first and second vertex
4208+ :rtype: tuple of Vertex subclass
4209+
4210+ ``v1`` and ``v2`` are None for an edge that has not been connected.
4211+ Every edge in a graph has both, so the code that works on them takes
4212+ them from here, which tells the type checker so, rather than testing
4213+ for None at each use. Unlike an ``assert`` this check is not removed
4214+ by ``python -O``.
4215+
4216+ :seealso: :attr:`v1` :attr:`v2`
4217+ """
4218+ v1 , v2 = self ._v1 , self ._v2
4219+ if v1 is None or v2 is None :
4220+ raise ValueError ("the edge is not connected to two vertices" )
4221+ return v1 , v2
4222+
42064223 def __reduce__ (self ) -> Any :
42074224 """
42084225 Refuse to pickle or copy an edge on its own
@@ -4334,12 +4351,11 @@ def next(self, vertex: Vertex) -> Vertex:
43344351
43354352 """
43364353
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
4354+ v1 , v2 = self ._ends ()
4355+ if v1 is vertex :
4356+ return v2
4357+ elif v2 is vertex :
4358+ return v1
43434359 else :
43444360 raise ValueError ("should not happen" )
43454361
@@ -4378,8 +4394,7 @@ def endpoints(self) -> list[Vertex]:
43784394
43794395 :seealso: :meth:`vertices`
43804396 """
4381- assert self .v1 is not None and self .v2 is not None
4382- return [self .v1 , self .v2 ]
4397+ return list (self ._ends ())
43834398
43844399 def remove (self ) -> None :
43854400 """
@@ -4804,7 +4819,8 @@ def _bind_edge(self, edge: Edge, dest: Vertex) -> None:
48044819 endpoints disagree with the vertices' edge lists.
48054820 """
48064821 graph = self ._graph
4807- assert graph is not None # connect() has checked
4822+ # type narrowing only: connect() has checked
4823+ assert graph is not None # nosec B101
48084824 if edge .v1 is None and edge .v2 is None :
48094825 edge ._set_endpoints (self , dest )
48104826 elif not graph ._endpoints_match (edge , self , dest ):
@@ -5141,6 +5157,25 @@ def _incident_edges(self) -> list[Edge]:
51415157 seen .setdefault (id (e ), e )
51425158 return list (seen .values ())
51435159
5160+ def _require_name (self ) -> str :
5161+ """
5162+ The name, raising clearly if the vertex has none (private method)
5163+
5164+ :raises ValueError: the vertex has no name
5165+ :return: the name
5166+ :rtype: str
5167+
5168+ A vertex that is not in a graph can have no name, but one in a graph
5169+ always has, so the code that works on the vertices of a graph takes
5170+ the name from here, which tells the type checker so. Unlike an
5171+ ``assert`` this check is not removed by ``python -O``.
5172+
5173+ :seealso: :attr:`name`
5174+ """
5175+ if self ._name is None :
5176+ raise ValueError ("the vertex has no name" )
5177+ return self ._name
5178+
51445179 @property
51455180 def name (self ) -> str | None :
51465181 """
0 commit comments