I'm trying to incorporate type checking in a Python module and I am stuck at the following error:
Type argument "builtins.dict[builtins.int, EdgeWeight`1]" of "GraphBase" must be a subtype of "Union[typing.MutableSet[builtins.int], typing.MutableMapping[builtins.int, so.EdgeWeightProtocol]]"
I want to support graphs with arbitrary edge weights objects, as long as they can be ordered. My attempt to annotate this was to define an EdgeWeight TypeVar bound to a EdgeWeightProtocol Protocol class. I understand that an EdgeWeight instance will not be a subclass of EdgeWeightProtocol, but it's a Protocol so it should have a ___lt___ method according to its definition.
Where is my understanding of Generics, TypeVars, and Protocols going wrong? Or is this a bug? How should this be supported?
I have been using mypy 0.910
import collections.abc
from abc import ABCMeta, abstractmethod
from collections import UserList
from typing import (
Any,
Callable,
Dict,
Iterable,
MutableMapping,
MutableSet,
Protocol,
Set,
Tuple,
TypeVar,
Union,
)
Edge = Tuple[int, int]
EdgeWeight = TypeVar("EdgeWeight", bound="EdgeWeightProtocol")
Graph = TypeVar("Graph", bound="GraphBase[Any]")
class EdgeWeightProtocol(Protocol):
def __lt__(self: EdgeWeight, other: EdgeWeight) -> bool:
...
S = TypeVar("S", bound=Union[MutableSet[int], MutableMapping[int, EdgeWeightProtocol]])
class GraphBase(UserList[S], metaclass=ABCMeta):
_item_type: Callable[..., S]
def __init__(self, n_or_initlist: Union[Iterable[S], int, None] = None,) -> None:
"""
Initializer for GraphBase.
Parameters
----------
n_or_initlist : int or iterable, optional
If `None` (default) initializes a graph of zero order and zero size
(i.e. no vertices and no edges). If int, initializes a graph of
order `n_or_initlist` and zero size. Otherwise initialize the graph
using the iterable `n_or_initlist`.
"""
if n_or_initlist is None:
super().__init__()
elif isinstance(n_or_initlist, int):
super().__init__((self._item_type() for _ in range(n_or_initlist)))
elif isinstance(n_or_initlist, collections.abc.Iterable):
super().__init__(map(self._item_type, n_or_initlist))
else:
raise ValueError(
f"Unsupported type '{type(n_or_initlist).__name__}', "
"expected int or iterable"
)
@abstractmethod
def remove_edge(self, edge: Edge) -> None:
pass
@abstractmethod
def get_edge_weight(self, edge: Edge) -> EdgeWeightProtocol:
pass
class WeightedGraph(GraphBase[Dict[int, EdgeWeight]]):
_item_type = dict
def add_edge(self, edge: Edge, weight: EdgeWeight) -> None:
j, i = edge
self.data[j][i] = weight
def remove_edge(self, edge: Edge) -> None:
j, i = edge
del self.data[j][i]
def get_edge_weight(self, edge: Edge) -> EdgeWeight:
j, i = edge
return self.data[j][i]
class UnweightedGraph(GraphBase[Set[int]]):
_item_type = set
def add_edge(self, edge: Edge) -> None:
j, i = edge
self.data[j].add(i)
def remove_edge(self, edge: Edge) -> None:
j, i = edge
self.data[j].remove(i)
def get_edge_weight(self, _edge: Edge) -> float:
# Always return an edge weight of 1 for an unweighted graph
return 1