diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b0e2ac10a..a6c597729 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -90,6 +90,31 @@ jobs: if: runner.os == 'Linux' - name: 'Run tests' run: tox -epy + tests_stubs: + needs: [tests] + name: python-stubs-${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8, 3.9, "3.10", "3.11"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: x86_64-unknown-linux-gnu + profile: minimal + default: true + - name: 'Install dependencies' + run: python -m pip install --upgrade 'tox<4' + - name: 'Run rustworkx stub tests' + run: tox -estubs tests_retworkx_compat: needs: [build_lint] name: python${{ matrix.python-version }}-${{ matrix.platform.python-architecture }} ${{ matrix.platform.os }} ${{ matrix.msrv }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 821b24929..bd6ebdaa7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -272,6 +272,21 @@ web browser by running: cargo doc --open ``` +### Type Annotations + +If you have added new methods or changed method signatures, we encourage you to add annotations for +those methods in stub files. The stub files are in the `rustworkx` directory and have a `.pyi` file extension. +They contain annotated signatures for Python functions, stripped of their implementation. + +While this step is optional, it is very helpful for end-users. Adding annotations lets users type check +their code with [mypy](http://mypy-lang.org/), which can be helpful for finding bugs. + +Just like with tests for the code, annotations are also tested via tox. + +``` +tox -estubs +``` + ### Release Notes It is important to document any end user facing changes when we release a new diff --git a/MANIFEST.in b/MANIFEST.in index 8cc086ccf..4df1daed0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,3 +4,4 @@ include Cargo.toml include Cargo.lock recursive-include src * recursive-include rustworkx-core * +recursive-include rustworkx *.pyi py.typed diff --git a/releasenotes/notes/graph-annotations-1d436930bf60c5c2.yaml b/releasenotes/notes/graph-annotations-1d436930bf60c5c2.yaml new file mode 100644 index 000000000..df7a94409 --- /dev/null +++ b/releasenotes/notes/graph-annotations-1d436930bf60c5c2.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added type annotations to :class:`~retworkx.PyDiGraph` and + :class:`~retworkx.PyGraph`. They can now be statically type checked + with `mypy `__. diff --git a/rustworkx/__init__.pyi b/rustworkx/__init__.pyi new file mode 100644 index 000000000..979e6df4f --- /dev/null +++ b/rustworkx/__init__.pyi @@ -0,0 +1,18 @@ +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# This file contains only type annotations for PyO3 functions and classes +# For implementation details, see __init__.py and src/lib.rs + +from .rustworkx import * +from typing import Generic, TypeVar + +S = TypeVar("S") +T = TypeVar("T") + +class PyDAG(Generic[S, T], PyDiGraph[S, T]): ... diff --git a/rustworkx/digraph.pyi b/rustworkx/digraph.pyi new file mode 100644 index 000000000..56b977d98 --- /dev/null +++ b/rustworkx/digraph.pyi @@ -0,0 +1,174 @@ +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# This file contains only type annotations for PyO3 functions and classes +# For implementation details, see __init__.py and src/digraph.rs + +import numpy as np +from .iterators import * +from .graph import PyGraph + +from typing import Any, Callable, Dict, Generic, TypeVar, Optional, List, Tuple, Sequence + +__all__ = ["PyDiGraph"] + +S = TypeVar("S") +T = TypeVar("T") + +class PyDiGraph(Generic[S, T]): + check_cycle: bool = ... + multigraph: bool = ... + def __init__( + self, + /, + check_cycle: bool = ..., + multigraph: bool = ..., + ) -> None: ... + def add_child(self, parent: int, obj: S, edge: T, /) -> int: ... + def add_edge(self, parent: int, child: int, edge: T, /) -> int: ... + def add_edges_from( + self, + obj_list: Sequence[Tuple[int, int, T]], + /, + ) -> List[int]: ... + def add_edges_from_no_data( + self: PyDiGraph[S, Optional[T]], obj_list: Sequence[Tuple[int, int]], / + ) -> List[int]: ... + def add_node(self, obj: S, /) -> int: ... + def add_nodes_from(self, obj_list: Sequence[S], /) -> NodeIndices: ... + def add_parent(self, child: int, obj: S, edge: T, /) -> int: ... + def adj(self, node: int, /) -> Dict[int, T]: ... + def adj_direction(self, node: int, direction: bool, /) -> Dict[int, T]: ... + def compose( + self, + other: PyDiGraph[S, T], + node_map: Dict[int, Tuple[int, T]], + /, + node_map_func: Optional[Callable[[S], int]] = ..., + edge_map_func: Optional[Callable[[T], int]] = ..., + ) -> Dict[int, int]: ... + def copy(self) -> PyDiGraph[S, T]: ... + def edge_index_map(self) -> EdgeIndexMap[T]: ... + def edge_indices(self) -> EdgeIndices: ... + def edge_list(self) -> EdgeList: ... + def edges(self) -> List[T]: ... + def extend_from_edge_list( + self: PyDiGraph[Optional[S], Optional[T]], edge_list: Sequence[Tuple[int, int]], / + ) -> None: ... + def extend_from_weighted_edge_list( + self: PyDiGraph[Optional[S], T], + edge_list: Sequence[Tuple[int, int, T]], + /, + ) -> None: ... + def find_adjacent_node_by_edge(self, node: int, predicate: Callable[[T], bool], /) -> S: ... + def find_node_by_weight( + self, + obj: Callable[[S], bool], + /, + ) -> Optional[int]: ... + def find_predecessors_by_edge( + self, node: int, filter_fn: Callable[[T], bool], / + ) -> List[S]: ... + def find_successors_by_edge(self, node: int, filter_fn: Callable[[T], bool], /) -> List[S]: ... + @staticmethod + def from_adjacency_matrix( + matrix: np.ndarray, /, null_value: float = ... + ) -> PyDiGraph[int, float]: ... + @staticmethod + def from_complex_adjacency_matrix( + matrix: np.ndarray, /, null_value: complex = ... + ) -> PyDiGraph[int, complex]: ... + def get_all_edge_data(self, node_a: int, node_b: int, /) -> List[T]: ... + def get_edge_data(self, node_a: int, node_b: int, /) -> T: ... + def get_node_data(self, node: int, /) -> S: ... + def has_edge(self, node_a: int, node_b: int, /) -> bool: ... + def in_degree(self, node: int, /) -> int: ... + def in_edges(self, node: int, /) -> WeightedEdgeList[T]: ... + def insert_node_on_in_edges(self, node: int, ref_node: int, /) -> None: ... + def insert_node_on_in_edges_multiple(self, node: int, ref_nodes: Sequence[int], /) -> None: ... + def insert_node_on_out_edges(self, node: int, ref_node: int, /) -> None: ... + def insert_node_on_out_edges_multiple(self, node: int, ref_nodes: Sequence[int], /) -> None: ... + def is_symmetric(self) -> bool: ... + def merge_nodes(self, u: int, v: int, /) -> None: ... + def neighbors(self, node: int, /) -> NodeIndices: ... + def node_indexes(self) -> NodeIndices: ... + def nodes(self) -> List[S]: ... + def num_edges(self) -> int: ... + def num_nodes(self) -> int: ... + def out_degree(self, node: int, /) -> int: ... + def out_edges(self, node: int, /) -> WeightedEdgeList[T]: ... + def predecessor_indices(self, node: int, /) -> NodeIndices: ... + def predecessors(self, node: int, /) -> List[S]: ... + @staticmethod + def read_edge_list( + path: str, + /, + comment: Optional[str] = ..., + deliminator: Optional[str] = ..., + labels: bool = ..., + ) -> PyDiGraph: ... + def remove_edge(self, parent: int, child: int, /) -> None: ... + def remove_edge_from_index(self, edge: int, /) -> None: ... + def remove_edges_from(self, index_list: Sequence[Tuple[int, int]], /) -> None: ... + def remove_node(self, node: int, /) -> None: ... + def remove_node_retain_edges( + self, + node: int, + /, + use_outgoing: Optional[bool] = ..., + condition: Optional[Callable[[S, S], bool]] = ..., + ) -> None: ... + def remove_nodes_from(self, index_list: Sequence[int], /) -> None: ... + def subgraph(self, nodes: Sequence[int], /, preserve_attrs: bool = ...) -> PyDiGraph[S, T]: ... + def substitute_node_with_subgraph( + self, + node: int, + other: PyDiGraph[S, T], + edge_map_fn: Callable[[int, int, T], Optional[int]], + /, + node_filter: Optional[Callable[[S], bool]] = ..., + edge_weight_map: Optional[Callable[[T], T]] = ..., + ) -> NodeMap: ... + def successor_indices(self, node: int, /) -> NodeIndices: ... + def successors(self, node: int, /) -> List[S]: ... + def to_dot( + self, + /, + node_attr: Optional[Callable[[S], Dict[str, str]]] = ..., + edge_attr: Optional[Callable[[T], Dict[str, str]]] = ..., + graph_attr: Optional[Dict[str, str]] = ..., + filename: Optional[str] = ..., + ) -> Optional[str]: ... + def to_undirected( + self, + /, + multigraph: bool = ..., + weight_combo_fn: Optional[Callable[[T, T], T]] = ..., + ) -> PyGraph[S, T]: ... + def update_edge( + self, + source: int, + target: int, + edge: T, + /, + ) -> None: ... + def update_edge_by_index(self, edge_index: int, edge: T, /) -> None: ... + def weighted_edge_list(self) -> WeightedEdgeList[T]: ... + def write_edge_list( + self, + path: str, + /, + deliminator: Optional[str] = ..., + weight_fn: Optional[Callable[[T], str]] = ..., + ) -> None: ... + def __delitem__(self, idx: int, /) -> None: ... + def __getitem__(self, idx: int, /) -> S: ... + def __getstate__(self) -> Any: ... + def __len__(self) -> int: ... + def __setitem__(self, idx: int, value: S, /) -> None: ... + def __setstate__(self, state, /) -> None: ... diff --git a/rustworkx/graph.pyi b/rustworkx/graph.pyi new file mode 100644 index 000000000..7277c1eda --- /dev/null +++ b/rustworkx/graph.pyi @@ -0,0 +1,129 @@ +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# This file contains only type annotations for PyO3 functions and classes +# For implementation details, see __init__.py and src/graph.rs + +import numpy as np +from .iterators import * + +from typing import ( + Any, + Callable, + Dict, + Generic, + TypeVar, + Optional, + List, + Tuple, + Sequence, +) + +__all__ = ["PyGraph"] + +S = TypeVar("S") +T = TypeVar("T") + +class PyGraph(Generic[S, T]): + multigraph: bool = ... + def __init__(self, /, multigraph: bool = ...) -> None: ... + def add_edge(self, node_a: int, node_b: int, edge: T, /) -> int: ... + def add_edges_from( + self, + obj_list: Sequence[Tuple[int, int, T]], + /, + ) -> List[int]: ... + def add_edges_from_no_data( + self: PyGraph[S, Optional[T]], obj_list: Sequence[Tuple[int, int]], / + ) -> List[int]: ... + def add_node(self, obj: S, /) -> int: ... + def add_nodes_from(self, obj_list: Sequence[S], /) -> NodeIndices: ... + def adj(self, node: int, /) -> Dict[int, T]: ... + def compose( + self, + other: PyGraph[S, T], + node_map: Dict[int, Tuple[int, T]], + /, + node_map_func: Optional[Callable[[S], int]] = ..., + edge_map_func: Optional[Callable[[T], int]] = ..., + ) -> Dict[int, int]: ... + def copy(self) -> PyGraph[S, T]: ... + def degree(self, node: int, /) -> int: ... + def edge_index_map(self) -> EdgeIndexMap[T]: ... + def edge_indices(self) -> EdgeIndices: ... + def edge_list(self) -> EdgeList: ... + def edges(self) -> List[T]: ... + def extend_from_edge_list( + self: PyGraph[Optional[S], Optional[T]], edge_list: Sequence[Tuple[int, int]], / + ) -> None: ... + def extend_from_weighted_edge_list( + self: PyGraph[Optional[S], T], + edge_list: Sequence[Tuple[int, int, T]], + /, + ) -> None: ... + @staticmethod + def from_adjacency_matrix( + matrix: np.ndarray, /, null_value: float = ... + ) -> PyGraph[int, float]: ... + @staticmethod + def from_complex_adjacency_matrix( + matrix: np.ndarray, /, null_value: complex = ... + ) -> PyGraph[int, complex]: ... + def get_all_edge_data(self, node_a: int, node_b: int, /) -> List[T]: ... + def get_edge_data(self, node_a: int, node_b: int, /) -> T: ... + def get_node_data(self, node: int, /) -> S: ... + def has_edge(self, node_a: int, node_b: int, /) -> bool: ... + def neighbors(self, node: int, /) -> NodeIndices: ... + def node_indexes(self) -> NodeIndices: ... + def nodes(self) -> List[S]: ... + def num_edges(self) -> int: ... + def num_nodes(self) -> int: ... + @staticmethod + def read_edge_list( + path: str, + /, + comment: Optional[str] = ..., + deliminator: Optional[str] = ..., + labels: bool = ..., + ) -> PyGraph: ... + def remove_edge(self, node_a: int, node_b: int, /) -> None: ... + def remove_edge_from_index(self, edge: int, /) -> None: ... + def remove_edges_from(self, index_list: Sequence[Tuple[int, int]], /) -> None: ... + def remove_node(self, node: int, /) -> None: ... + def remove_nodes_from(self, index_list: Sequence[int], /) -> None: ... + def subgraph(self, nodes: Sequence[int], /, preserve_attrs: bool = ...) -> PyGraph[S, T]: ... + def to_dot( + self, + /, + node_attr: Optional[Callable[[S], Dict[str, str]]] = ..., + edge_attr: Optional[Callable[[T], Dict[str, str]]] = ..., + graph_attr: Optional[Dict[str, str]] = ..., + filename: Optional[str] = ..., + ) -> Optional[str]: ... + def update_edge( + self, + source: int, + target: int, + edge: T, + /, + ) -> None: ... + def update_edge_by_index(self, edge_index: int, edge: T, /) -> None: ... + def weighted_edge_list(self) -> WeightedEdgeList[T]: ... + def write_edge_list( + self, + path: str, + /, + deliminator: Optional[str] = ..., + weight_fn: Optional[Callable[[T], str]] = ..., + ) -> None: ... + def __delitem__(self, idx: int, /) -> None: ... + def __getitem__(self, idx: int, /) -> S: ... + def __getstate__(self) -> Any: ... + def __len__(self) -> int: ... + def __setitem__(self, idx: int, value: S, /) -> None: ... + def __setstate__(self, state, /) -> None: ... diff --git a/rustworkx/iterators.pyi b/rustworkx/iterators.pyi new file mode 100644 index 000000000..81840d84b --- /dev/null +++ b/rustworkx/iterators.pyi @@ -0,0 +1,144 @@ +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# This file contains only type annotations for PyO3 functions and classes +# For implementation details, see __init__.py and src/iterators.rs + +from typing import ( + Any, + Generic, + List, + Dict, + ItemsView, + KeysView, + ValuesView, + Iterator, + Mapping, + TypeVar, + Tuple, + overload, + final, +) +from abc import ABC +from collections.abc import Sequence +from typing_extensions import Self + +import numpy as np + +S = TypeVar("S") +T_co = TypeVar("T_co", covariant=True) + +__all__ = [ + "NodeIndices", + "PathLengthMapping", + "PathMapping", + "AllPairsPathLengthMapping", + "AllPairsPathMapping", + "BFSSuccessors", + "EdgeIndexMap", + "EdgeIndices", + "Chains", + "EdgeList", + "NodeMap", + "NodesCountMapping", + "Pos2DMapping", + "WeightedEdgeList", + "CentralityMapping", + "BiconnectedComponents", + "ProductNodeMap", + "MultiplePathMapping", + "AllPairsMultiplePathMapping", +] + +class RustworkxCustomVecIter(Generic[T_co], Sequence[T_co], ABC): + def __init__(self) -> None: ... + def __eq__(self, other: object) -> bool: ... + @overload + def __getitem__(self, index: int) -> T_co: ... + @overload + def __getitem__(self: Self, index: slice) -> Self: ... + def __getstate__(self) -> Any: ... + def __hash__(self) -> int: ... + def __str__(self) -> str: ... + def __len__(self) -> int: ... + def __ne__(self, other: object) -> bool: ... + def __setstate__(self, state: Sequence[T_co]) -> None: ... + def __array__(self, _dt: np.dtype = ...) -> np.ndarray: ... + +class RustworkxCustomHashMapIter(Generic[S, T_co], Mapping[S, T_co], ABC): + def __init__(self) -> None: ... + def items(self) -> ItemsView[S, T_co]: ... + def keys(self) -> KeysView[S]: ... + def values(self) -> ValuesView[T_co]: ... + def __contains__(self, other: object) -> bool: ... + def __eq__(self, other: object) -> bool: ... + def __getitem__(self, index: S) -> T_co: ... + def __getstate__(self) -> Any: ... + def __hash__(self) -> int: ... + def __str__(self) -> str: ... + def __iter__(self) -> Iterator[S]: ... + def __len__(self) -> int: ... + def __ne__(self, other: object) -> bool: ... + def __setstate__(self, state: Mapping[S, T_co]) -> None: ... + +@final +class NodeIndices(RustworkxCustomVecIter[int]): ... + +@final +class PathLengthMapping(RustworkxCustomHashMapIter[int, float]): ... + +@final +class PathMapping(RustworkxCustomHashMapIter[int, NodeIndices]): ... + +@final +class AllPairsPathLengthMapping(RustworkxCustomHashMapIter[int, PathLengthMapping]): ... + +@final +class AllPairsPathMapping(RustworkxCustomHashMapIter[int, PathMapping]): ... + +@final +class BFSSuccessors(Generic[T_co], RustworkxCustomVecIter[Tuple[T_co, List[T_co]]]): ... + +@final +class EdgeIndexMap(Generic[T_co], RustworkxCustomHashMapIter[int, Tuple[int, int, T_co]]): ... + +@final +class EdgeIndices(RustworkxCustomVecIter[int]): ... + +@final +class Chains(RustworkxCustomVecIter[EdgeIndices]): ... + +@final +class EdgeList(RustworkxCustomVecIter[Tuple[int, int]]): ... + +@final +class NodeMap(RustworkxCustomHashMapIter[int, int]): ... + +@final +class NodesCountMapping(RustworkxCustomHashMapIter[int, int]): ... + +@final +class Pos2DMapping(RustworkxCustomHashMapIter[int, Tuple[float, float]]): ... + +@final +class WeightedEdgeList(Generic[T_co], RustworkxCustomVecIter[Tuple[int, int, T_co]]): ... + +@final +class CentralityMapping(RustworkxCustomHashMapIter[int, float]): ... + +@final +class BiconnectedComponents(RustworkxCustomHashMapIter[Tuple[int, int], int]): ... + +@final +class ProductNodeMap(RustworkxCustomHashMapIter[Tuple[int, int], int]): ... + +@final +class MultiplePathMapping(RustworkxCustomHashMapIter[int, List[List[int]]]): ... + +@final +class AllPairsMultiplePathMapping(RustworkxCustomHashMapIter[int, MultiplePathMapping]): ... diff --git a/rustworkx/py.typed b/rustworkx/py.typed new file mode 100644 index 000000000..b648ac923 --- /dev/null +++ b/rustworkx/py.typed @@ -0,0 +1 @@ +partial diff --git a/rustworkx/rustworkx.pyi b/rustworkx/rustworkx.pyi new file mode 100644 index 000000000..d0924ccac --- /dev/null +++ b/rustworkx/rustworkx.pyi @@ -0,0 +1,25 @@ +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# This file contains only type annotations for PyO3 functions and classes +# For implementation details, see __init__.py and src/lib.rs + +from .iterators import * +from .graph import PyGraph as PyGraph +from .digraph import PyDiGraph as PyDiGraph + +class DAGHasCycle(Exception): ... +class DAGWouldCycle(Exception): ... +class InvalidNode(Exception): ... +class NoEdgeBetweenNodes(Exception): ... +class NoPathFound(Exception): ... +class NoSuitableNeighbors(Exception): ... +class NullGraph(Exception): ... +class NegativeCycle(Exception): ... +class JSONSerializationError(Exception): ... +class FailedToConverge(Exception): ... diff --git a/src/digraph.rs b/src/digraph.rs index 0fcddde12..f02a1ed1d 100644 --- a/src/digraph.rs +++ b/src/digraph.rs @@ -733,7 +733,7 @@ impl PyDiGraph { /// :param int target: The index for the second node /// /// :raises NoEdgeBetweenNodes: When there is no edge between nodes - #[pyo3(text_signature = "(self, source, target, edge /)")] + #[pyo3(text_signature = "(self, source, target, edge, /)")] pub fn update_edge(&mut self, source: usize, target: usize, edge: PyObject) -> PyResult<()> { let index_a = NodeIndex::new(source); let index_b = NodeIndex::new(target); @@ -1066,7 +1066,7 @@ impl PyDiGraph { /// ``(source, target, weight)`` where source and target are integer /// node indices. If the node index is not present in the graph /// nodes will be added (with a node weight of ``None``) to that index. - #[pyo3(text_signature = "(self, edge_lsit, /)")] + #[pyo3(text_signature = "(self, edge_list, /)")] pub fn extend_from_weighted_edge_list( &mut self, py: Python, @@ -1271,7 +1271,7 @@ impl PyDiGraph { /// /// :param int u: The source node that is going to be merged /// :param int v: The target node that is going to be the new node - #[pyo3(text_signature = "(self, u, v /)")] + #[pyo3(text_signature = "(self, u, v, /)")] pub fn merge_nodes(&mut self, py: Python, u: usize, v: usize) -> PyResult<()> { let source_node = NodeIndex::new(u); let target_node = NodeIndex::new(v); diff --git a/src/graph.rs b/src/graph.rs index c718ce930..3b0f2c43a 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -579,7 +579,7 @@ impl PyGraph { /// :param int target: The index for the second node /// /// :raises NoEdgeBetweenNodes: When there is no edge between nodes - #[pyo3(text_signature = "(self, source, target, edge /)")] + #[pyo3(text_signature = "(self, source, target, edge, /)")] pub fn update_edge(&mut self, source: usize, target: usize, edge: PyObject) -> PyResult<()> { let index_a = NodeIndex::new(source); let index_b = NodeIndex::new(target); @@ -850,7 +850,7 @@ impl PyGraph { /// ``(source, target, weight)`` where source and target are integer /// node indices. If the node index is not present in the graph, /// nodes will be added (with a node weight of ``None``) to that index. - #[pyo3(text_signature = "(self, edge_lsit, /)")] + #[pyo3(text_signature = "(self, edge_list, /)")] pub fn extend_from_weighted_edge_list( &mut self, py: Python, diff --git a/tox.ini b/tox.ini index 646c1bfe6..8f5e71656 100644 --- a/tox.ini +++ b/tox.ini @@ -56,6 +56,13 @@ deps = black~=22.0 commands = black {posargs} '../rustworkx' '../tests' '../retworkx' +[testenv:stubs] +basepython = python3 +envdir = .tox/stubs +deps = + mypy==1.0.1 +commands = python -m mypy.stubtest --concise --ignore-missing-stub rustworkx.rustworkx + [flake8] # E125 is deliberately excluded. See https://github.com/jcrocholl/pep8/issues/126 # E123 skipped because it is ignored by default in the default pep8