diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 103821c..6758b93 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,16 +21,34 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Conda + - name: Setup mamba uses: conda-incubator/setup-miniconda@v2 + id: setup_mamba + continue-on-error: true + with: + miniforge-variant: Mambaforge + miniforge-version: latest + use-mamba: true + python-version: ${{ matrix.python-version }} + channels: conda-forge,${{ contains(matrix.python-version, 'pypy') && 'defaults' || 'nodefaults' }} + channel-priority: ${{ contains(matrix.python-version, 'pypy') && 'flexible' || 'strict' }} + activate-environment: graphblas + auto-activate-base: false + - name: Setup conda + uses: conda-incubator/setup-miniconda@v2 + id: setup_conda + if: steps.setup_mamba.outcome == 'failure' + continue-on-error: false with: auto-update-conda: true python-version: ${{ matrix.python-version }} - channels: conda-forge - activate-environment: testing + channels: conda-forge,${{ contains(matrix.python-version, 'pypy') && 'defaults' || 'nodefaults' }} + channel-priority: ${{ contains(matrix.python-version, 'pypy') && 'flexible' || 'strict' }} + activate-environment: graphblas + auto-activate-base: false - name: Install dependencies run: | - conda install -c conda-forge python-graphblas scipy pandas pytest-cov pytest-randomly pytest-mpl + $(command -v mamba || command -v conda) install python-graphblas scipy pandas pytest-cov pytest-randomly pytest-mpl # matplotlib lxml pygraphviz pydot sympy # Extra networkx deps we don't need yet pip install git+https://github.com/networkx/networkx.git@main --no-deps pip install -e . --no-deps diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b08ce0..474539b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,21 +5,23 @@ # To update: `pre-commit autoupdate` # - &flake8_dependencies below needs updated manually ci: - # See: https://pre-commit.ci/#configuration - autofix_prs: false - autoupdate_schedule: quarterly - skip: [no-commit-to-branch] + # See: https://pre-commit.ci/#configuration + autofix_prs: false + autoupdate_schedule: quarterly + autoupdate_commit_msg: "chore: update pre-commit hooks" + autofix_commit_msg: "style: pre-commit fixes" + skip: [no-commit-to-branch] fail_fast: true default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - - id: check-symlinks + # - id: check-symlinks - id: check-ast - id: check-toml - id: check-yaml @@ -37,7 +39,7 @@ repos: name: Validate pyproject.toml # I don't yet trust ruff to do what autoflake does - repo: https://github.com/PyCQA/autoflake - rev: v2.2.0 + rev: v2.2.1 hooks: - id: autoflake args: [--in-place] @@ -46,22 +48,22 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.15.0 hooks: - id: pyupgrade - args: [--py38-plus] + args: [--py39-plus] - repo: https://github.com/MarcoGorelli/auto-walrus rev: v0.2.2 hooks: - id: auto-walrus args: [--line-length, "100"] - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.9.1 hooks: - id: black # - id: black-jupyter - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.285 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.292 hooks: - id: ruff args: [--fix-only, --show-fixes] @@ -72,22 +74,22 @@ repos: additional_dependencies: &flake8_dependencies # These versions need updated manually - flake8==6.1.0 - - flake8-bugbear==23.7.10 - - flake8-simplify==0.20.0 + - flake8-bugbear==23.9.16 + - flake8-simplify==0.21.0 - repo: https://github.com/asottile/yesqa rev: v1.5.0 hooks: - id: yesqa additional_dependencies: *flake8_dependencies - repo: https://github.com/codespell-project/codespell - rev: v2.2.5 + rev: v2.2.6 hooks: - id: codespell types_or: [python, rst, markdown] additional_dependencies: [tomli] files: ^(graphblas_algorithms|docs)/ - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.285 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.292 hooks: - id: ruff # `pyroma` may help keep our package standards up to date if best practices change. @@ -98,6 +100,6 @@ repos: - id: pyroma args: [-n, "10", .] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: no-commit-to-branch # no commit directly to main diff --git a/_nx_graphblas/__init__.py b/_nx_graphblas/__init__.py new file mode 100644 index 0000000..c3cc602 --- /dev/null +++ b/_nx_graphblas/__init__.py @@ -0,0 +1,107 @@ +def get_info(): + return { + "backend_name": "graphblas", + "project": "graphblas-algorithms", + "package": "graphblas_algorithms", + "url": "https://github.com/python-graphblas/graphblas-algorithms", + "short_summary": "Fast, OpenMP-enabled backend using GraphBLAS", + # "description": "TODO", + "functions": { + "adjacency_matrix": {}, + "all_pairs_bellman_ford_path_length": { + "extra_parameters": { + "chunksize": "Split the computation into chunks; " + 'may specify size as string or number of rows. Default "10 MiB"', + }, + }, + "all_pairs_shortest_path_length": { + "extra_parameters": { + "chunksize": "Split the computation into chunks; " + 'may specify size as string or number of rows. Default "10 MiB"', + }, + }, + "ancestors": {}, + "average_clustering": {}, + "bellman_ford_path": {}, + "bellman_ford_path_length": {}, + "bethe_hessian_matrix": {}, + "bfs_layers": {}, + "boundary_expansion": {}, + "clustering": {}, + "complement": {}, + "compose": {}, + "conductance": {}, + "cut_size": {}, + "degree_centrality": {}, + "descendants": {}, + "descendants_at_distance": {}, + "difference": {}, + "directed_modularity_matrix": {}, + "disjoint_union": {}, + "edge_boundary": {}, + "edge_expansion": {}, + "efficiency": {}, + "ego_graph": {}, + "eigenvector_centrality": {}, + "fast_could_be_isomorphic": {}, + "faster_could_be_isomorphic": {}, + "floyd_warshall": {}, + "floyd_warshall_numpy": {}, + "floyd_warshall_predecessor_and_distance": {}, + "full_join": {}, + "generalized_degree": {}, + "google_matrix": {}, + "has_path": {}, + "hits": {}, + "in_degree_centrality": {}, + "inter_community_edges": {}, + "intersection": {}, + "intra_community_edges": {}, + "is_connected": {}, + "is_dominating_set": {}, + "is_isolate": {}, + "is_k_regular": {}, + "isolates": {}, + "is_regular": {}, + "is_simple_path": {}, + "is_tournament": {}, + "is_triad": {}, + "is_weakly_connected": {}, + "katz_centrality": {}, + "k_truss": {}, + "laplacian_matrix": {}, + "lowest_common_ancestor": {}, + "mixing_expansion": {}, + "modularity_matrix": {}, + "mutual_weight": {}, + "negative_edge_cycle": {}, + "node_boundary": {}, + "node_connected_component": {}, + "node_expansion": {}, + "normalized_cut_size": {}, + "normalized_laplacian_matrix": {}, + "number_of_isolates": {}, + "out_degree_centrality": {}, + "overall_reciprocity": {}, + "pagerank": {}, + "reciprocity": {}, + "reverse": {}, + "score_sequence": {}, + "single_source_bellman_ford_path_length": {}, + "single_source_shortest_path_length": {}, + "single_target_shortest_path_length": {}, + "s_metric": {}, + "square_clustering": { + "extra_parameters": { + "chunksize": "Split the computation into chunks; " + 'may specify size as string or number of rows. Default "256 MiB"', + }, + }, + "symmetric_difference": {}, + "tournament_matrix": {}, + "transitivity": {}, + "triangles": {}, + "union": {}, + "volume": {}, + }, + } diff --git a/graphblas_algorithms/classes/digraph.py b/graphblas_algorithms/classes/digraph.py index 8da9c8a..1e9fe5f 100644 --- a/graphblas_algorithms/classes/digraph.py +++ b/graphblas_algorithms/classes/digraph.py @@ -442,6 +442,7 @@ def __missing__(self, key): class DiGraph(Graph): + __networkx_backend__ = "graphblas" __networkx_plugin__ = "graphblas" # "-" properties ignore self-edges, "+" properties include self-edges @@ -611,7 +612,7 @@ def to_undirected(self, reciprocal=False, as_view=False, *, name=None): return Graph(B, key_to_id=self._key_to_id) def reverse(self, copy=True): - # We could even re-use many of the cached values + # We could even reuse many of the cached values A = self._A.T # This probably mostly works, but does not yet support assignment if copy: A = A.new() diff --git a/graphblas_algorithms/classes/graph.py b/graphblas_algorithms/classes/graph.py index 06f82be..f3e2239 100644 --- a/graphblas_algorithms/classes/graph.py +++ b/graphblas_algorithms/classes/graph.py @@ -301,6 +301,7 @@ def __missing__(self, key): class Graph: + __networkx_backend__ = "graphblas" __networkx_plugin__ = "graphblas" # "-" properties ignore self-edges, "+" properties include self-edges diff --git a/graphblas_algorithms/nxapi/smetric.py b/graphblas_algorithms/nxapi/smetric.py index a363e1e..a1f60ab 100644 --- a/graphblas_algorithms/nxapi/smetric.py +++ b/graphblas_algorithms/nxapi/smetric.py @@ -1,13 +1,22 @@ +import warnings + from graphblas_algorithms import algorithms from graphblas_algorithms.classes.digraph import to_graph -from .exception import NetworkXError - __all__ = ["s_metric"] -def s_metric(G, normalized=True): - if normalized: - raise NetworkXError("Normalization not implemented") +def s_metric(G, **kwargs): + if kwargs: + if "normalized" in kwargs: + warnings.warn( + "\n\nThe `normalized` keyword is deprecated and will be removed\n" + "in the future. To silence this warning, remove `normalized`\n" + "when calling `s_metric`.\n\nThe value of `normalized` is ignored.", + DeprecationWarning, + stacklevel=2, + ) + else: + raise TypeError(f"s_metric got an unexpected keyword argument '{kwargs.popitem()[0]}'") G = to_graph(G) return algorithms.s_metric(G) diff --git a/graphblas_algorithms/tests/test_core.py b/graphblas_algorithms/tests/test_core.py index 5acd529..68dbeb7 100644 --- a/graphblas_algorithms/tests/test_core.py +++ b/graphblas_algorithms/tests/test_core.py @@ -27,6 +27,7 @@ def test_packages(): path = pathlib.Path(ga.__file__).parent pkgs = [f"graphblas_algorithms.{x}" for x in setuptools.find_packages(path)] pkgs.append("graphblas_algorithms") + pkgs.append("_nx_graphblas") pkgs.sort() pyproject = path.parent / "pyproject.toml" if not pyproject.exists(): diff --git a/pyproject.toml b/pyproject.toml index 8fb2ffc..78b07ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ name = "graphblas-algorithms" dynamic = ["version"] description = "Graph algorithms written in GraphBLAS and backend for NetworkX" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" license = {file = "LICENSE"} authors = [ {name = "Erik Welch", email = "erik.n.welch@gmail.com"}, @@ -43,7 +43,6 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -65,6 +64,12 @@ dependencies = [ [project.entry-points."networkx.plugins"] graphblas = "graphblas_algorithms.interface:Dispatcher" +[project.entry-points."networkx.backends"] +graphblas = "graphblas_algorithms.interface:Dispatcher" + +[project.entry-points."networkx.backend_info"] +graphblas = "_nx_graphblas:get_info" + [project.urls] homepage = "https://github.com/python-graphblas/graphblas-algorithms" # documentation = "https://graphblas-algorithms.readthedocs.io" @@ -90,6 +95,7 @@ all = [ # $ find graphblas_algorithms/ -name __init__.py -print | sort | sed -e 's/\/__init__.py//g' -e 's/\//./g' # $ python -c 'import tomli ; [print(x) for x in sorted(tomli.load(open("pyproject.toml", "rb"))["tool"]["setuptools"]["packages"])]' packages = [ + "_nx_graphblas", "graphblas_algorithms", "graphblas_algorithms.algorithms", "graphblas_algorithms.algorithms.centrality", @@ -127,7 +133,7 @@ dirty_template = "{tag}+{ccount}.g{sha}.dirty" [tool.black] line-length = 100 -target-version = ["py38", "py39", "py310", "py311"] +target-version = ["py39", "py310", "py311"] [tool.isort] sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] @@ -143,6 +149,7 @@ skip = [ ] [tool.pytest.ini_options] +minversion = "6.0" testpaths = "graphblas_algorithms" xfail_strict = false markers = [ @@ -169,7 +176,10 @@ exclude_lines = [ [tool.ruff] # https://github.com/charliermarsh/ruff/ line-length = 100 -target-version = "py38" +target-version = "py39" +unfixable = [ + "F841" # unused-variable (Note: can leave useless expression) +] select = [ "ALL", ]