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_array
 
To 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)