Skip to content

Commit 0cf0dd0

Browse files
authored
Merge branch 'main' into chore/housekeeping-leftovers
2 parents 347b7c8 + bffb7be commit 0cf0dd0

4 files changed

Lines changed: 217 additions & 80 deletions

File tree

‎src/pgraph/PGraph.py‎

Lines changed: 118 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)