Skip to content

merge networkx Graph classes from python-type-stubs and address a few recent issues #14597

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions stubs/networkx/networkx/algorithms/planarity.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from _typeshed import Incomplete
from collections.abc import Generator, Mapping, MutableSet, Reversible
from _typeshed import Incomplete, Unused
from collections.abc import Generator, Iterable, Mapping, MutableSet, Reversible
from typing import NoReturn

from networkx.classes.digraph import DiGraph
from networkx.classes.graph import Graph, _Node
from networkx.classes.graph import Graph, _EdgePlus, _Node
from networkx.utils.backends import _dispatchable

__all__ = ["check_planarity", "is_planar", "PlanarEmbedding"]
Expand Down Expand Up @@ -67,6 +68,9 @@ class LRPlanarity:
def sign(self, e): ...
def sign_recursive(self, e): ...

# NOTE: Graph subclasses relationships are so complex
# we're only overriding methods that differ in signature from the base classes
# to use inheritance to our advantage and reduce complexity
class PlanarEmbedding(DiGraph[_Node]):
def get_data(self) -> dict[_Node, list[_Node]]: ...
def set_data(self, data: Mapping[_Node, Reversible[_Node]]) -> None: ...
Expand All @@ -81,4 +85,9 @@ class PlanarEmbedding(DiGraph[_Node]):
def traverse_face(
self, v: _Node, w: _Node, mark_half_edges: MutableSet[tuple[_Node, _Node]] | None = None
) -> list[_Node]: ...
def to_undirected(self, reciprocal: bool = False, as_view: bool = False) -> Graph[_Node]: ... # type: ignore[override]
# Overriden in __init__ to always raise
def add_edge(self, u_of_edge: _Node, v_of_edge: _Node, **attr: Unused) -> NoReturn: ...
def add_edges_from(self, ebunch_to_add: Iterable[_EdgePlus[_Node]], **attr: Unused) -> NoReturn: ...
def add_weighted_edges_from(
self, ebunch_to_add: Iterable[tuple[_Node, _Node, float]], weight: str = "weight", **attr: Unused
) -> NoReturn: ...
20 changes: 13 additions & 7 deletions stubs/networkx/networkx/classes/digraph.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ from networkx.classes.reportviews import (
InDegreeView,
InEdgeView,
InMultiDegreeView,
InMultiEdgeView,
OutDegreeView,
OutEdgeView,
OutMultiDegreeView,
)

__all__ = ["DiGraph"]

# NOTE: Graph subclasses relationships are so complex
# we're only overriding methods that differ in signature from the base classes
# to use inheritance to our advantage and reduce complexity
class DiGraph(Graph[_Node]):
@cached_property
def succ(self) -> AdjacencyView[_Node, _Node, dict[str, Any]]: ...
Expand All @@ -30,17 +34,19 @@ class DiGraph(Graph[_Node]):

def predecessors(self, n: _Node) -> Iterator[_Node]: ...
@cached_property
def edges(self) -> OutEdgeView[_Node]: ...
@cached_property
def out_edges(self) -> OutEdgeView[_Node]: ...
@cached_property
def in_edges(self) -> InEdgeView[_Node]: ...
# Including subtypes' possible return types for LSP
def in_edges(self) -> InEdgeView[_Node] | InMultiEdgeView[_Node]: ...
@cached_property
def degree(self) -> DiDegreeView[_Node]: ...
@cached_property
# Including subtypes' possible return types for LSP
def in_degree(self) -> InDegreeView[_Node] | InMultiDegreeView[_Node]: ...
@cached_property
# Including subtypes' possible return types for LSP
def out_degree(self) -> OutDegreeView[_Node] | OutMultiDegreeView[_Node]: ...
def to_undirected(self, reciprocal: bool = False, as_view: bool = False) -> Graph[_Node]: ... # type: ignore[override]
# reciprocal : If True, only edges that appear in both directions ... will be kept in the undirected graph.
def to_undirected(self, reciprocal: bool = False, as_view: bool = False) -> Graph[_Node]: ... # type: ignore[override] # Has an additional `reciprocal` keyword argument
def reverse(self, copy: bool = True) -> Self: ...
@cached_property
def edges(self) -> OutEdgeView[_Node]: ... # type: ignore[override] # An OutEdgeView of the DiGraph as G.edges or G.edges().
@cached_property
def degree(self) -> int | DiDegreeView[_Node]: ... # type: ignore[override] # Returns DiDegreeView or int
15 changes: 9 additions & 6 deletions stubs/networkx/networkx/classes/graph.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ from typing_extensions import Self, TypeAlias
import numpy
from networkx.classes.coreviews import AdjacencyView, AtlasView
from networkx.classes.digraph import DiGraph
from networkx.classes.reportviews import DegreeView, EdgeView, NodeView
from networkx.classes.reportviews import DegreeView, DiDegreeView, EdgeView, NodeView, OutEdgeView

_Node = TypeVar("_Node", bound=Hashable)
_NodeWithData: TypeAlias = tuple[_Node, dict[str, Any]]
Expand Down Expand Up @@ -49,10 +49,10 @@ class Graph(Collection[_Node]):
def name(self) -> str: ...
@name.setter
def name(self, s: str) -> None: ...
def __getitem__(self, n: _Node) -> AtlasView[_Node, str, Any]: ...
def __iter__(self) -> Iterator[_Node]: ...
def __contains__(self, n: object) -> bool: ...
def __len__(self) -> int: ...
def __getitem__(self, n: _Node) -> AtlasView[_Node, str, Any]: ...
def add_node(self, node_for_adding: _Node, **attr: Any) -> None: ... # attr: Set or change node attributes using key=value
def add_nodes_from(self, nodes_for_adding: Iterable[_NodePlus[_Node]], **attr: Any) -> None: ... # attr: key=value pairs
def remove_node(self, n: _Node) -> None: ...
Expand All @@ -62,7 +62,8 @@ class Graph(Collection[_Node]):
def number_of_nodes(self) -> int: ...
def order(self) -> int: ...
def has_node(self, n: _Node) -> bool: ...
def add_edge(self, u_of_edge: _Node, v_of_edge: _Node, **attr: Any) -> None: ...
# Including subtypes' possible return types for LSP
def add_edge(self, u_of_edge: _Node, v_of_edge: _Node, **attr: Any) -> Hashable | None: ...
# attr: Edge data (or labels or objects) can be assigned using keyword arguments
def add_edges_from(self, ebunch_to_add: Iterable[_EdgePlus[_Node]], **attr: Any) -> None: ...
# attr: Edge data (or labels or objects) can be assigned using keyword arguments
Expand All @@ -81,20 +82,22 @@ class Graph(Collection[_Node]):
def has_edge(self, u: _Node, v: _Node) -> bool: ...
def neighbors(self, n: _Node) -> Iterator[_Node]: ...
@cached_property
def edges(self) -> EdgeView[_Node]: ...
# Including subtypes' possible return types for LSP
def edges(self) -> EdgeView[_Node] | OutEdgeView[_Node]: ...
def get_edge_data(self, u: _Node, v: _Node, default: Any = None) -> dict[str, Any]: ...
# default: any Python object
def adjacency(self) -> Iterator[tuple[_Node, dict[_Node, dict[str, Any]]]]: ...
@cached_property
def degree(self) -> int | DegreeView[_Node]: ...
# Including subtypes' possible return types for LSP
def degree(self) -> DegreeView[_Node] | DiDegreeView[_Node]: ...
def clear(self) -> None: ...
def clear_edges(self) -> None: ...
def is_multigraph(self) -> bool: ...
def is_directed(self) -> bool: ...
def copy(self, as_view: bool = False) -> Self: ...
def to_directed(self, as_view: bool = False) -> DiGraph[_Node]: ...
def to_undirected(self, as_view: bool = False) -> Graph[_Node]: ...
def subgraph(self, nodes: Iterable[_Node]) -> Graph[_Node]: ...
def subgraph(self, nodes: _NBunch[_Node]) -> Graph[_Node]: ...
def edge_subgraph(self, edges: Iterable[_Edge[_Node]]) -> Graph[_Node]: ...
@overload
def size(self, weight: None = None) -> int: ...
Expand Down
19 changes: 10 additions & 9 deletions stubs/networkx/networkx/classes/multidigraph.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,36 @@ from typing import Any

from networkx.classes.coreviews import MultiAdjacencyView
from networkx.classes.digraph import DiGraph
from networkx.classes.graph import _EdgeWithData, _Node
from networkx.classes.graph import _Node
from networkx.classes.multigraph import MultiGraph
from networkx.classes.reportviews import (
DiMultiDegreeView,
InMultiDegreeView,
InMultiEdgeDataView,
InMultiEdgeView,
OutMultiDegreeView,
OutMultiEdgeView,
)

__all__ = ["MultiDiGraph"]

# NOTE: Graph subclasses relationships are so complex
# we're only overriding methods that differ in signature from the base classes
# to use inheritance to our advantage and reduce complexity
class MultiDiGraph(MultiGraph[_Node], DiGraph[_Node]):
@cached_property
def succ(self) -> MultiAdjacencyView[_Node, _Node, dict[str, Any]]: ...
@cached_property
def pred(self) -> MultiAdjacencyView[_Node, _Node, dict[str, Any]]: ...
@cached_property
def edges(self) -> OutMultiEdgeView[_Node]: ... # type: ignore[override]
# Returns: OutMultiEdgeView
def edges(self) -> OutMultiEdgeView[_Node]: ...
@cached_property
def out_edges(self) -> OutMultiEdgeView[_Node]: ...
@cached_property
def in_edges(self) -> InMultiEdgeView[_Node] | InMultiEdgeDataView[_Node, _EdgeWithData[_Node]]: ... # type: ignore[override]
# Returns : InMultiEdgeView or InMultiEdgeDataView
def in_edges(self) -> InMultiEdgeView[_Node]: ...
@cached_property
def degree(self) -> DiMultiDegreeView[_Node]: ...
@cached_property
def in_degree(self) -> InMultiDegreeView[_Node]: ...
@cached_property
def out_degree(self) -> OutMultiDegreeView[_Node]: ...
def to_undirected(self, reciprocal: bool = False, as_view: bool = False) -> MultiGraph[_Node]: ... # type: ignore[override]
def reverse(self, copy: bool = True) -> MultiDiGraph[_Node]: ...
def copy(self, as_view: bool = False) -> MultiDiGraph[_Node]: ...
def to_undirected(self, reciprocal: bool = False, as_view: bool = False) -> MultiGraph[_Node]: ... # type: ignore[override] # Has an additional `reciprocal` keyword argument
31 changes: 19 additions & 12 deletions stubs/networkx/networkx/classes/multigraph.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ from typing_extensions import TypeAlias, TypeVar
from networkx.classes.coreviews import MultiAdjacencyView
from networkx.classes.graph import Graph, _MapFactory, _Node
from networkx.classes.multidigraph import MultiDiGraph
from networkx.classes.reportviews import MultiEdgeView
from networkx.classes.reportviews import DiMultiDegreeView, MultiDegreeView, MultiEdgeView, OutMultiEdgeView

_MultiEdge: TypeAlias = tuple[_Node, _Node, int] # noqa: Y047

Expand All @@ -15,36 +15,43 @@ _KeyT = TypeVar("_KeyT", bound=Hashable)

__all__ = ["MultiGraph"]

# NOTE: Graph subclasses relationships are so complex
# we're only overriding methods that differ in signature from the base classes
# to use inheritance to our advantage and reduce complexity
class MultiGraph(Graph[_Node]):
edge_key_dict_factory: ClassVar[_MapFactory]
def to_directed_class(self) -> type[MultiDiGraph[_Node]]: ...
def to_undirected_class(self) -> type[MultiGraph[_Node]]: ...
def __init__(self, incoming_graph_data=None, multigraph_input: bool | None = None, **attr: Any) -> None: ...
@cached_property
def adj(self) -> MultiAdjacencyView[_Node, _Node, dict[str, Any]]: ... # data can be any type
def new_edge_key(self, u: _Node, v: _Node) -> int: ...
@overload # type: ignore[override] # Has an additional `key` keyword argument
# key : hashable identifier, optional (default=lowest unused integer)
@overload # type: ignore[override] # More complex overload
def add_edge(self, u_for_edge: _Node, v_for_edge: _Node, key: int | None = None, **attr: Any) -> int: ...
@overload
def add_edge(self, u_for_edge: _Node, v_for_edge: _Node, key: _KeyT, **attr: Any) -> _KeyT: ...
# key : hashable identifier, optional (default=lowest unused integer)
def remove_edge(self, u: _Node, v: _Node, key: Hashable | None = None) -> None: ...
def has_edge(self, u: _Node, v: _Node, key: Hashable | None = None) -> bool: ...
@cached_property
# Including subtypes' possible return types for LSP
def edges(self) -> MultiEdgeView[_Node] | OutMultiEdgeView[_Node]: ...
# key : hashable identifier, optional (default=None).
# default : any Python object (default=None). Value to return if the specific edge (u, v, key) is not found.
# Returns: The edge attribute dictionary.
@overload # type: ignore[override]
def get_edge_data(
self, u: _Node, v: _Node, key: Hashable, default: _DefaultT | None = None
) -> dict[str, Any] | _DefaultT: ...
# key : hashable identifier, optional (default=None).
# default : any Python object (default=None). Value to return if the specific edge (u, v, key) is not found.
# Returns: The edge attribute dictionary.
# default : any Python object (default=None). Value to return if there are no edges between u and v and no key is specified.
# Returns: A dictionary mapping edge keys to attribute dictionaries for each of those edges if no specific key is provided.
@overload
def get_edge_data(
self, u: _Node, v: _Node, key: None = None, default: _DefaultT | None = None
) -> dict[Hashable, dict[str, Any] | _DefaultT]: ...
# default : any Python object (default=None). Value to return if there are no edges between u and v and no key is specified.
# Returns: A dictionary mapping edge keys to attribute dictionaries for each of those edges if no specific key is provided.
def copy(self, as_view: bool = False) -> MultiGraph[_Node]: ...
@cached_property
# Including subtypes' possible return types for LSP
def degree(self) -> MultiDegreeView[_Node] | DiMultiDegreeView[_Node]: ...
def to_directed(self, as_view: bool = False) -> MultiDiGraph[_Node]: ...
def to_undirected(self, as_view: bool = False) -> MultiGraph[_Node]: ...
def number_of_edges(self, u: _Node | None = None, v: _Node | None = None) -> int: ...
@cached_property
def edges(self) -> MultiEdgeView[_Node]: ... # type: ignore[override]
# Returns: MultiEdgeView
17 changes: 10 additions & 7 deletions stubs/networkx/networkx/classes/reportviews.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ class NodeView(Mapping[_Node, dict[str, Any]], AbstractSet[_Node]):
@overload
def __call__(self, data: Literal[False] = False, default=None) -> Self: ...
@overload
def __call__(self, data: Literal[True] | str, default=None) -> Self: ...
def data(self, data: bool | str = True, default=None) -> Self: ...
def __call__(self, data: Literal[True] | str, default=None) -> NodeDataView[_Node]: ...
@overload
def data(self, data: Literal[False], default=None) -> Self: ...
@overload
def data(self, data: Literal[True] | str = True, default=None) -> NodeDataView[_Node]: ...

class NodeDataView(AbstractSet[_Node]):
def __init__(self, nodedict: Mapping[str, Incomplete], data: bool | str = False, default=None) -> None: ...
Expand All @@ -55,12 +58,12 @@ class NodeDataView(AbstractSet[_Node]):

class DiDegreeView(Generic[_Node]):
def __init__(self, G: Graph[_Node], nbunch: _NBunch[_Node] = None, weight: None | bool | str = None) -> None: ...
@overload # Use this overload first in case _Node=str, since `str` matches `Iterable[str]`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is worth a test in check_tricky_function_params.py?

Copy link
Collaborator Author

@Avasam Avasam Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. I was also pondering if I should add a test or if a comment is enough.

I guess both don't hurt.

def __call__(self, nbunch: _Node, weight: None | bool | str = None) -> int: ... # type: ignore[overload-overlap]
@overload
def __call__(self, nbunch: None = None, weight: None | bool | str = None) -> int: ... # type: ignore[overload-overlap]
@overload
def __call__(self, nbunch: None | Iterable[_Node], weight: None | bool | str = None) -> Self: ...
def __getitem__(self, n: _Node) -> float: ...
def __iter__(self) -> Iterator[tuple[_Node, float]]: ...
def __call__(self, nbunch: Iterable[_Node] | None = None, weight: None | bool | str = None) -> Self: ...
def __getitem__(self, n: _Node) -> int: ...
def __iter__(self) -> Iterator[tuple[_Node, int]]: ...
Comment on lines +61 to +66
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've included this file because of #14595 (comment)

But if reviewer is uncertain about these changes, I can split it off

def __len__(self) -> int: ...

class DegreeView(DiDegreeView[_Node]): ...
Expand Down
Loading