diff --git a/.gitignore b/.gitignore index 358650cfc5a..d9c19ab0f12 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,8 @@ datasets/* !datasets/karate-disjoint.csv !datasets/netscience.csv +# nx-python side effects +python/nx-cugraph/objects.inv .pydevproject diff --git a/python/nx-cugraph/Makefile b/python/nx-cugraph/Makefile index c7c717a67fb..bd278ee477a 100644 --- a/python/nx-cugraph/Makefile +++ b/python/nx-cugraph/Makefile @@ -16,6 +16,9 @@ lint-update: plugin-info: python _nx_cugraph/__init__.py +objects.inv: + wget https://networkx.org/documentation/latest/objects.inv + .PHONY: readme -readme: - python scripts/update_readme.py README.md +readme: objects.inv + python scripts/update_readme.py README.md objects.inv diff --git a/python/nx-cugraph/README.md b/python/nx-cugraph/README.md index ed61831a7e0..60125d09f2a 100644 --- a/python/nx-cugraph/README.md +++ b/python/nx-cugraph/README.md @@ -91,143 +91,143 @@ familiar and easy-to-use API. Below is the list of algorithms that are currently supported in nx-cugraph. -### Algorithms +### [Algorithms](https://networkx.org/documentation/latest/reference/algorithms/index.html)
-bipartite - ├─ basic - │ └─ is_bipartite - └─ generators - └─ complete_bipartite_graph -centrality - ├─ betweenness - │ ├─ betweenness_centrality - │ └─ edge_betweenness_centrality - ├─ degree_alg - │ ├─ degree_centrality - │ ├─ in_degree_centrality - │ └─ out_degree_centrality - ├─ eigenvector - │ └─ eigenvector_centrality - └─ katz - └─ katz_centrality -cluster - ├─ average_clustering - ├─ clustering - ├─ transitivity - └─ triangles -community - └─ louvain - └─ louvain_communities -components - ├─ connected - │ ├─ connected_components - │ ├─ is_connected - │ ├─ node_connected_component - │ └─ number_connected_components - └─ weakly_connected - ├─ is_weakly_connected - ├─ number_weakly_connected_components - └─ weakly_connected_components -core - ├─ core_number - └─ k_truss -dag - ├─ ancestors - └─ descendants -isolate - ├─ is_isolate - ├─ isolates - └─ number_of_isolates -link_analysis - ├─ hits_alg - │ └─ hits - └─ pagerank_alg - └─ pagerank -operators - └─ unary - ├─ complement - └─ reverse -reciprocity - ├─ overall_reciprocity - └─ reciprocity -shortest_paths - └─ unweighted - ├─ single_source_shortest_path_length - └─ single_target_shortest_path_length -traversal - └─ breadth_first_search - ├─ bfs_edges - ├─ bfs_layers - ├─ bfs_predecessors - ├─ bfs_successors - ├─ bfs_tree - ├─ descendants_at_distance - └─ generic_bfs_edges -tree - └─ recognition - ├─ is_arborescence - ├─ is_branching - ├─ is_forest - └─ is_tree +bipartite + ├─ basic + │ └─ is_bipartite + └─ generators + └─ complete_bipartite_graph +centrality + ├─ betweenness + │ ├─ betweenness_centrality + │ └─ edge_betweenness_centrality + ├─ degree_alg + │ ├─ degree_centrality + │ ├─ in_degree_centrality + │ └─ out_degree_centrality + ├─ eigenvector + │ └─ eigenvector_centrality + └─ katz + └─ katz_centrality +cluster + ├─ average_clustering + ├─ clustering + ├─ transitivity + └─ triangles +community + └─ louvain + └─ louvain_communities +components + ├─ connected + │ ├─ connected_components + │ ├─ is_connected + │ ├─ node_connected_component + │ └─ number_connected_components + └─ weakly_connected + ├─ is_weakly_connected + ├─ number_weakly_connected_components + └─ weakly_connected_components +core + ├─ core_number + └─ k_truss +dag + ├─ ancestors + └─ descendants +isolate + ├─ is_isolate + ├─ isolates + └─ number_of_isolates +link_analysis + ├─ hits_alg + │ └─ hits + └─ pagerank_alg + └─ pagerank +operators + └─ unary + ├─ complement + └─ reverse +reciprocity + ├─ overall_reciprocity + └─ reciprocity +shortest_paths + └─ unweighted + ├─ single_source_shortest_path_length + └─ single_target_shortest_path_length +traversal + └─ breadth_first_search + ├─ bfs_edges + ├─ bfs_layers + ├─ bfs_predecessors + ├─ bfs_successors + ├─ bfs_tree + ├─ descendants_at_distance + └─ generic_bfs_edges +tree + └─ recognition + ├─ is_arborescence + ├─ is_branching + ├─ is_forest + └─ is_tree-### Generators +### [Generators](https://networkx.org/documentation/latest/reference/generators.html)
-classic - ├─ barbell_graph - ├─ circular_ladder_graph - ├─ complete_graph - ├─ complete_multipartite_graph - ├─ cycle_graph - ├─ empty_graph - ├─ ladder_graph - ├─ lollipop_graph - ├─ null_graph - ├─ path_graph - ├─ star_graph - ├─ tadpole_graph - ├─ trivial_graph - ├─ turan_graph - └─ wheel_graph -community - └─ caveman_graph -small - ├─ bull_graph - ├─ chvatal_graph - ├─ cubical_graph - ├─ desargues_graph - ├─ diamond_graph - ├─ dodecahedral_graph - ├─ frucht_graph - ├─ heawood_graph - ├─ house_graph - ├─ house_x_graph - ├─ icosahedral_graph - ├─ krackhardt_kite_graph - ├─ moebius_kantor_graph - ├─ octahedral_graph - ├─ pappus_graph - ├─ petersen_graph - ├─ sedgewick_maze_graph - ├─ tetrahedral_graph - ├─ truncated_cube_graph - ├─ truncated_tetrahedron_graph - └─ tutte_graph -social - ├─ davis_southern_women_graph - ├─ florentine_families_graph - ├─ karate_club_graph - └─ les_miserables_graph +classic + ├─ barbell_graph + ├─ circular_ladder_graph + ├─ complete_graph + ├─ complete_multipartite_graph + ├─ cycle_graph + ├─ empty_graph + ├─ ladder_graph + ├─ lollipop_graph + ├─ null_graph + ├─ path_graph + ├─ star_graph + ├─ tadpole_graph + ├─ trivial_graph + ├─ turan_graph + └─ wheel_graph +community + └─ caveman_graph +small + ├─ bull_graph + ├─ chvatal_graph + ├─ cubical_graph + ├─ desargues_graph + ├─ diamond_graph + ├─ dodecahedral_graph + ├─ frucht_graph + ├─ heawood_graph + ├─ house_graph + ├─ house_x_graph + ├─ icosahedral_graph + ├─ krackhardt_kite_graph + ├─ moebius_kantor_graph + ├─ octahedral_graph + ├─ pappus_graph + ├─ petersen_graph + ├─ sedgewick_maze_graph + ├─ tetrahedral_graph + ├─ truncated_cube_graph + ├─ truncated_tetrahedron_graph + └─ tutte_graph +social + ├─ davis_southern_women_graph + ├─ florentine_families_graph + ├─ karate_club_graph + └─ les_miserables_graph### Other
-convert_matrix - ├─ from_pandas_edgelist - └─ from_scipy_sparse_array +convert_matrix + ├─ from_pandas_edgelist + └─ from_scipy_sparse_arrayTo request nx-cugraph backend support for a NetworkX API that is not listed diff --git a/python/nx-cugraph/nx_cugraph/scripts/print_tree.py b/python/nx-cugraph/nx_cugraph/scripts/print_tree.py index 573ffa2ff14..fbb1c3dd0c5 100755 --- a/python/nx-cugraph/nx_cugraph/scripts/print_tree.py +++ b/python/nx-cugraph/nx_cugraph/scripts/print_tree.py @@ -31,7 +31,16 @@ def assoc_in(d, keys, value): return d -def tree_lines(tree, parents=(), are_levels_closing=()): +def default_get_payload_internal(keys): + return keys[-1] + + +def tree_lines( + tree, + parents=(), + are_levels_closing=(), + get_payload_internal=default_get_payload_internal, +): pre = "".join( " " if is_level_closing else " │ " for is_level_closing in are_levels_closing @@ -45,8 +54,13 @@ def tree_lines(tree, parents=(), are_levels_closing=()): if isinstance(val, str): yield pre + f" {c}─ " + val else: - yield pre + f" {c}─ " + key - yield from tree_lines(val, (parents, *key), are_levels_closing) + yield pre + f" {c}─ " + get_payload_internal((*parents, key)) + yield from tree_lines( + val, + (*parents, key), + are_levels_closing, + get_payload_internal=get_payload_internal, + ) def get_payload( @@ -99,6 +113,7 @@ def create_tree( different=False, prefix="", strip_networkx=True, + get_payload=get_payload, ): if path_to_info is None: path_to_info = get_path_to_info() diff --git a/python/nx-cugraph/scripts/update_readme.py b/python/nx-cugraph/scripts/update_readme.py index e2b50d12ad6..7b7f4b8b29d 100644 --- a/python/nx-cugraph/scripts/update_readme.py +++ b/python/nx-cugraph/scripts/update_readme.py @@ -13,24 +13,126 @@ # limitations under the License. import argparse import re +import zlib +from collections import namedtuple from pathlib import Path +from warnings import warn from nx_cugraph.scripts.print_tree import create_tree, tree_lines +# See: https://sphobjinv.readthedocs.io/en/stable/syntax.html +DocObject = namedtuple( + "DocObject", + "name, domain, role, priority, uri, displayname", +) + + +def parse_docobject(line): + left, right = line.split(":") + name, domain = left.rsplit(" ", 1) + role, priority, uri, displayname = right.split(" ", 3) + if displayname == "-": + displayname = name + if uri.endswith("$"): + uri = uri[:-1] + name + return DocObject(name, domain, role, priority, uri, displayname) + def replace_body(text, match, new_body): start, stop = match.span("body") return text[:start] + new_body + text[stop:] -def main(file): - """``file`` must be readable and writable, so use mode ``"a+"``""" - file.seek(0) - text = file.read() - tree = create_tree() +# NetworkX isn't perfectly intersphinx-compatible, so manually specify some urls. +# TODO: create networkx issue about intersphinx incompatibility +MANUAL_OBJECT_URLS = { + "networkx.algorithms.centrality.betweenness": ( + "https://networkx.org/documentation/latest/reference/" + "algorithms/centrality.html#shortest-path-betweenness" + ), + "networkx.algorithms.centrality.degree_alg": ( + "https://networkx.org/documentation/latest/reference/" + "algorithms/centrality.html#degree" + ), + "networkx.algorithms.centrality.eigenvector": ( + "https://networkx.org/documentation/latest/reference/" + "algorithms/centrality.html#eigenvector" + ), + "networkx.algorithms.centrality.katz": ( + "https://networkx.org/documentation/latest/reference/" + "algorithms/centrality.html#eigenvector" + ), + "networkx.algorithms.components.connected": ( + "https://networkx.org/documentation/latest/reference/" + "algorithms/component.html#connectivity" + ), + "networkx.algorithms.components.weakly_connected": ( + "https://networkx.org/documentation/latest/reference/" + "algorithms/component.html#weak-connectivity" + ), +} + + +def main(readme_file, objects_filename): + """``readme_file`` must be readable and writable, so use mode ``"a+"``""" + # Use the `objects.inv` file to determine URLs. For details about this file, see: + # https://sphobjinv.readthedocs.io/en/stable/syntax.html + # We might be better off using a library like that, but roll our own for now. + with Path(objects_filename).open("rb") as objects_file: + line = objects_file.readline() + if line != b"# Sphinx inventory version 2\n": + raise RuntimeError(f"Bad line in objects.inv:\n\n{line}") + line = objects_file.readline() + if line != b"# Project: NetworkX\n": + raise RuntimeError(f"Bad line in objects.inv:\n\n{line}") + line = objects_file.readline() + if not line.startswith(b"# Version: "): + raise RuntimeError(f"Bad line in objects.inv:\n\n{line}") + line = objects_file.readline() + if line != b"# The remainder of this file is compressed using zlib.\n": + raise RuntimeError(f"Bad line in objects.inv:\n\n{line}") + zlib_data = objects_file.read() + objects_text = zlib.decompress(zlib_data).decode().strip() + objects_list = [parse_docobject(line) for line in objects_text.split("\n")] + doc_urls = { + # Should we use latest or stable? + obj.name: "https://networkx.org/documentation/latest/" + obj.uri + for obj in objects_list + } + if len(objects_list) != len(doc_urls): + raise RuntimeError("Oops; duplicate names found in objects.inv") + + def get_payload(info, **kwargs): + path = "networkx." + info.networkx_path + subpath, name = path.rsplit(".", 1) + # Many objects are referred to in modules above where they are defined. + while subpath: + path = f"{subpath}.{name}" + if path in doc_urls: + return f'{name}' + subpath = subpath.rsplit(".", 1)[0] + warn(f"Unable to find URL for {name!r}: {path}", stacklevel=0) + return name + + def get_payload_internal(keys): + path = "networkx." + ".".join(keys) + name = keys[-1] + if path in doc_urls: + return f'{name}' + path2 = "reference/" + "/".join(keys) + if path2 in doc_urls: + return f'{name}' + if path in MANUAL_OBJECT_URLS: + return f'{name}' + warn(f"Unable to find URL for {name!r}: {path}", stacklevel=0) + return name + + readme_file.seek(0) + text = readme_file.read() + tree = create_tree(get_payload=get_payload) # Algorithms match = re.search( - r"### Algorithms\n(?P
\n(?P.*?)\n", + r"### .Algorithms(?P
\n(?P.*?)\n", text, re.DOTALL, ) @@ -38,12 +140,18 @@ def main(file): raise RuntimeError("Algorithms section not found!") lines = [] for key, val in tree["algorithms"].items(): - lines.append(key) - lines.extend(tree_lines(val, parents=("algorithms", key))) + lines.append(get_payload_internal(("algorithms", key))) + lines.extend( + tree_lines( + val, + parents=("algorithms", key), + get_payload_internal=get_payload_internal, + ) + ) text = replace_body(text, match, "\n".join(lines)) # Generators match = re.search( - r"### Generators\n(?P
\n(?P.*?)\n", + r"### .Generators(?P
\n(?P.*?)\n", text, re.DOTALL, ) @@ -51,8 +159,14 @@ def main(file): raise RuntimeError("Generators section not found!") lines = [] for key, val in tree["generators"].items(): - lines.append(key) - lines.extend(tree_lines(val, parents=("generators", key))) + lines.append(get_payload_internal(("generators", key))) + lines.extend( + tree_lines( + val, + parents=("generators", key), + get_payload_internal=get_payload_internal, + ) + ) text = replace_body(text, match, "\n".join(lines)) # Other match = re.search( @@ -66,12 +180,14 @@ def main(file): for key, val in tree.items(): if key in {"algorithms", "generators"}: continue - lines.append(key) - lines.extend(tree_lines(val, parents=(key,))) + lines.append(get_payload_internal((key,))) + lines.extend( + tree_lines(val, parents=(key,), get_payload_internal=get_payload_internal) + ) text = replace_body(text, match, "\n".join(lines)) # Now overwrite README.md - file.truncate(0) - file.write(text) + readme_file.truncate(0) + readme_file.write(text) return text @@ -80,6 +196,9 @@ def main(file): "Update README.md to show NetworkX functions implemented by nx-cugraph" ) parser.add_argument("readme_filename", help="Path to the README.md file") + parser.add_argument( + "networkx_objects", help="Path to the objects.inv file from networkx docs" + ) args = parser.parse_args() - with Path(args.readme_filename).open("a+") as f: - main(f) + with Path(args.readme_filename).open("a+") as readme_file: + main(readme_file, args.networkx_objects)