@@ -1482,14 +1482,14 @@ def remove_vertex(self, vertex: Vertex) -> None:
14821482 """
14831483 if vertex ._graph is not self :
14841484 raise ValueError ("vertex does not belong to this graph" )
1485- assert vertex . name is not None
1485+ name = vertex . _require_name ()
14861486
14871487 # its own lists name every edge at it, so no need to search the graph
14881488 for edge in vertex ._incident_edges ():
14891489 self .remove_edge (edge )
14901490
14911491 self ._vertexlist .remove (vertex )
1492- del self ._vertexdict [vertex . name ]
1492+ del self ._vertexdict [name ]
14931493 vertex ._graph = None # it is not in any graph now
14941494 vertex ._label = None # and so not in a component
14951495 self ._connectivitychange = True
@@ -2273,7 +2273,8 @@ def near(self, coord: ArrayLike, radius: float) -> list[tuple[Vertex, float]]:
22732273
22742274 found = []
22752275 for vertex in candidates :
2276- 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
22772278 dist = float (self .metric (vertex .coord - coord ))
22782279 if dist <= radius :
22792280 found .append ((vertex , dist ))
@@ -2339,19 +2340,19 @@ def _edge_layout(self) -> tuple[dict[Edge, float], dict[Edge, int]]:
23392340 groups : dict [frozenset [int ], list [Edge ]] = {}
23402341 loops : dict [Vertex , list [Edge ]] = {}
23412342 for e in self ._edges :
2342- assert e . v1 is not None and e . v2 is not None
2343- if e . v1 is e . v2 :
2344- loops .setdefault (e . v1 , []).append (e )
2343+ v1 , v2 = e . _ends ()
2344+ if v1 is v2 :
2345+ loops .setdefault (v1 , []).append (e )
23452346 else :
2346- groups .setdefault (frozenset ((index [e . v1 ], index [e . v2 ])), []).append (e )
2347+ groups .setdefault (frozenset ((index [v1 ], index [v2 ])), []).append (e )
23472348 rads : dict [Edge , float ] = {}
23482349 for edges in groups .values ():
23492350 n = len (edges )
23502351 for k , e in enumerate (edges ):
2351- assert e . v1 is not None and e . v2 is not None
2352+ v1 , v2 = e . _ends ()
23522353 offset = (k - (n - 1 ) / 2 ) * _ARC_STEP
23532354 # the same side in space for an edge drawn the other way round
2354- rads [e ] = offset if index [e . v1 ] < index [e . v2 ] else - offset
2355+ rads [e ] = offset if index [v1 ] < index [v2 ] else - offset
23552356 loopno = {e : k for edges in loops .values () for k , e in enumerate (edges )}
23562357 return rads , loopno
23572358
@@ -2380,8 +2381,7 @@ def _edge_artist(
23802381 from matplotlib .patches import FancyArrowPatch
23812382 from matplotlib .transforms import Affine2D , ScaledTranslation
23822383 from ._plot import _Arrow3D , _Loop3D , _loop_path
2383- v1 , v2 = edge .v1 , edge .v2
2384- assert v1 is not None and v2 is not None
2384+ v1 , v2 = edge ._ends ()
23852385 shrink = markersize / 2 + 1
23862386 if v1 is v2 :
23872387 radius = _LOOP_RADIUS + _LOOP_GROWTH * loopno
@@ -2627,10 +2627,10 @@ def plot(
26272627 # the edges, each once
26282628 rads , loopno = self ._edge_layout ()
26292629 for e in self ._edges :
2630- assert e . v1 is not None
2630+ start = e . _ends ()[ 0 ]
26312631 kwargs = dict (edge_kwargs )
26322632 if color_components :
2633- kwargs = {** dict (color = color [e . v1 .label , :]), ** kwargs }
2633+ kwargs = {** dict (color = color [start .label , :]), ** kwargs }
26342634 artist = self ._edge_artist (
26352635 ax ,
26362636 e ,
@@ -2656,10 +2656,8 @@ def plot(
26562656 )
26572657 dx , dy = abs (x1 - x0 ), abs (y1 - y0 )
26582658 for e in loopno :
2659- assert e .v1 is not None
2660- ax .update_datalim (
2661- [(e .v1 .x - dx , e .v1 .y - dy ), (e .v1 .x + dx , e .v1 .y + dy )]
2662- )
2659+ v = e ._ends ()[0 ]
2660+ ax .update_datalim ([(v .x - dx , v .y - dy ), (v .x + dx , v .y + dy )])
26632661 ax .autoscale_view ()
26642662
26652663 ax .grid (grid )
@@ -3443,11 +3441,11 @@ def incidence(self) -> NDArray:
34433441 vdict [vertex ] = i
34443442
34453443 for j , e in enumerate (edges ):
3446- assert e . v1 is not None and e . v2 is not None
3447- I [vdict [e . v1 ], j ] = 1
3448- I [vdict [e . v2 ], j ] = 1
3449- if e . v1 is e . v2 and not self .directed :
3450- 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
34513449
34523450 return I
34533451
@@ -3800,8 +3798,7 @@ def path_UCS(
38003798
38013799 parent_names : dict [str , str ] = {}
38023800 for v , p in parent .items ():
3803- assert v .name is not None and p .name is not None
3804- parent_names [v .name ] = p .name
3801+ parent_names [v ._require_name ()] = p ._require_name ()
38053802
38063803 if summary or verbose :
38073804 print (
@@ -3938,8 +3935,7 @@ def path_Astar(
39383935
39393936 parent_names : dict [str , str ] = {}
39403937 for v , p in parent .items ():
3941- assert v .name is not None and p .name is not None
3942- parent_names [v .name ] = p .name
3938+ parent_names [v ._require_name ()] = p ._require_name ()
39433939
39443940 if summary or verbose :
39453941 print (
@@ -3980,9 +3976,9 @@ def _attach(self, edge: Edge) -> None:
39803976 from either end. A self-loop is therefore held twice by its vertex,
39813977 which is why it counts 2 towards the degree.
39823978 """
3983- assert edge . v1 is not None and edge .v2 is not None
3984- edge . v1 ._edgelist .append (edge )
3985- edge . v2 ._edgelist .append (edge )
3979+ v1 , v2 = edge ._ends ()
3980+ v1 ._edgelist .append (edge )
3981+ v2 ._edgelist .append (edge )
39863982
39873983 def _detach (self , edge : Edge ) -> None :
39883984 """
@@ -4035,9 +4031,9 @@ def _attach(self, edge: Edge) -> None:
40354031 The edge goes in the start vertex's list of outgoing edges and in the
40364032 end vertex's list of incoming ones.
40374033 """
4038- assert edge . v1 is not None and edge .v2 is not None
4039- edge . v1 ._edgelist .append (edge )
4040- edge . v2 ._inlist .append (edge )
4034+ v1 , v2 = edge ._ends ()
4035+ v1 ._edgelist .append (edge )
4036+ v2 ._inlist .append (edge )
40414037
40424038 def _detach (self , edge : Edge ) -> None :
40434039 """
@@ -4210,6 +4206,27 @@ def _set_endpoints(self, v1: Vertex | None, v2: Vertex | None) -> None:
42104206 self ._v1 = v1
42114207 self ._v2 = v2
42124208
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+
42134230 def __reduce__ (self ) -> Any :
42144231 """
42154232 Refuse to pickle or copy an edge on its own
@@ -4341,12 +4358,11 @@ def next(self, vertex: Vertex) -> Vertex:
43414358
43424359 """
43434360
4344- if self .v1 is vertex :
4345- assert self .v2 is not None
4346- return self .v2
4347- elif self .v2 is vertex :
4348- assert self .v1 is not None
4349- return self .v1
4361+ v1 , v2 = self ._ends ()
4362+ if v1 is vertex :
4363+ return v2
4364+ elif v2 is vertex :
4365+ return v1
43504366 else :
43514367 raise ValueError ("should not happen" )
43524368
@@ -4385,8 +4401,7 @@ def endpoints(self) -> list[Vertex]:
43854401
43864402 :seealso: :meth:`vertices`
43874403 """
4388- assert self .v1 is not None and self .v2 is not None
4389- return [self .v1 , self .v2 ]
4404+ return list (self ._ends ())
43904405
43914406 def remove (self ) -> None :
43924407 """
@@ -4811,7 +4826,8 @@ def _bind_edge(self, edge: Edge, dest: Vertex) -> None:
48114826 endpoints disagree with the vertices' edge lists.
48124827 """
48134828 graph = self ._graph
4814- assert graph is not None # connect() has checked
4829+ # type narrowing only: connect() has checked
4830+ assert graph is not None # nosec B101
48154831 if edge .v1 is None and edge .v2 is None :
48164832 edge ._set_endpoints (self , dest )
48174833 elif not graph ._endpoints_match (edge , self , dest ):
@@ -5148,6 +5164,25 @@ def _incident_edges(self) -> list[Edge]:
51485164 seen .setdefault (id (e ), e )
51495165 return list (seen .values ())
51505166
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+
51515186 @property
51525187 def name (self ) -> str | None :
51535188 """
0 commit comments