diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..4fd4800 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,14 @@ +coverage: + status: + project: + default: + informational: true + patch: + default: + informational: true + changes: false +comment: + layout: "header, diff" + behavior: default +github_checks: + annotations: false diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 1970710..d9889a1 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.9" + python-version: "3.10" - name: Install build dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e04e92f..8be0379 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: fail-fast: true matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11", "3.12"] steps: - name: Checkout uses: actions/checkout@v4 @@ -49,9 +49,10 @@ jobs: - name: Install dependencies run: | $(command -v mamba || command -v conda) install python-suitesparse-graphblas scipy pandas donfig pyyaml numpy python-graphblas \ - pytest-cov pytest-randomly pytest-mpl + pytest-cov pytest-randomly pytest-mpl networkx # 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 + # Sometimes we prefer to use the latest release of NetworkX or the latest development from github + # pip install git+https://github.com/networkx/networkx.git@main --no-deps pip install -e . --no-deps - name: PyTest run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55021f2..c9e708b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,26 +44,26 @@ repos: - id: autoflake args: [--in-place] - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.1 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade rev: v3.15.0 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py310-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.10.0 + rev: 23.12.0 hooks: - id: black # - id: black-jupyter - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.0 + rev: v0.1.7 hooks: - id: ruff args: [--fix-only, --show-fixes] @@ -74,7 +74,7 @@ repos: additional_dependencies: &flake8_dependencies # These versions need updated manually - flake8==6.1.0 - - flake8-bugbear==23.9.16 + - flake8-bugbear==23.12.2 - flake8-simplify==0.21.0 - repo: https://github.com/asottile/yesqa rev: v1.5.0 @@ -89,7 +89,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas_algorithms|docs)/ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.0 + rev: v0.1.7 hooks: - id: ruff # `pyroma` may help keep our package standards up to date if best practices change. diff --git a/graphblas_algorithms/algorithms/operators/binary.py b/graphblas_algorithms/algorithms/operators/binary.py index 11b5b19..4c14a11 100644 --- a/graphblas_algorithms/algorithms/operators/binary.py +++ b/graphblas_algorithms/algorithms/operators/binary.py @@ -67,7 +67,7 @@ def intersection(G, H, *, name="intersection"): ids = H.list_to_ids(keys) B = H._A[ids, ids].new(dtypes.unify(A.dtype, H._A.dtype), mask=A.S, name=name) B << unary.one(B) - return type(G)(B, key_to_id=dict(zip(keys, range(len(keys))))) + return type(G)(B, key_to_id=dict(zip(keys, range(len(keys)), strict=True))) def difference(G, H, *, name="difference"): @@ -142,7 +142,7 @@ def compose(G, H, *, name="compose"): C[A.nrows :, A.ncols :] = B[ids, ids] # Now make new `key_to_id` ids += A.nrows - key_to_id = dict(zip(newkeys, ids.tolist())) + key_to_id = dict(zip(newkeys, ids.tolist(), strict=True)) key_to_id.update(G._key_to_id) return type(G)(C, key_to_id=key_to_id) diff --git a/graphblas_algorithms/algorithms/shortest_paths/weighted.py b/graphblas_algorithms/algorithms/shortest_paths/weighted.py index 0c2883c..a83a060 100644 --- a/graphblas_algorithms/algorithms/shortest_paths/weighted.py +++ b/graphblas_algorithms/algorithms/shortest_paths/weighted.py @@ -199,7 +199,7 @@ def bellman_ford_path_lengths(G, nodes=None, *, expand_output=False): def _reconstruct_path_from_parents(G, parents, src, dst): indices, values = parents.to_coo(sort=False) - d = dict(zip(indices.tolist(), values.tolist())) + d = dict(zip(indices.tolist(), values.tolist(), strict=True)) if dst not in d: return [] cur = dst diff --git a/graphblas_algorithms/classes/_utils.py b/graphblas_algorithms/classes/_utils.py index d15a188..ecf66d9 100644 --- a/graphblas_algorithms/classes/_utils.py +++ b/graphblas_algorithms/classes/_utils.py @@ -61,7 +61,7 @@ def dict_to_vector(self, d, *, size=None, dtype=None, name=None): if size is None: size = len(self) key_to_id = self._key_to_id - indices, values = zip(*((key_to_id[key], val) for key, val in d.items())) + indices, values = zip(*((key_to_id[key], val) for key, val in d.items()), strict=True) return Vector.from_coo(indices, values, size=size, dtype=dtype, name=name) @@ -116,7 +116,7 @@ def vector_to_dict(self, v, *, mask=None, fill_value=None): elif fill_value is not None and v.nvals < v.size: v(mask=~v.S) << fill_value id_to_key = self.id_to_key - return {id_to_key[index]: value for index, value in zip(*v.to_coo(sort=False))} + return {id_to_key[index]: value for index, value in zip(*v.to_coo(sort=False), strict=True)} def vector_to_list(self, v, *, values_are_keys=False): @@ -198,26 +198,29 @@ def matrix_to_dicts(self, A, *, use_row_index=False, use_column_index=False, val id_to_key = self.id_to_key if values_are_keys: values = [id_to_key[val] for val in values] - it = zip(rows, np.lib.stride_tricks.sliding_window_view(indptr, 2).tolist()) + it = zip(rows, np.lib.stride_tricks.sliding_window_view(indptr, 2).tolist(), strict=True) if use_row_index and use_column_index: return { - row: dict(zip(col_indices[start:stop], values[start:stop])) for row, (start, stop) in it + row: dict(zip(col_indices[start:stop], values[start:stop], strict=True)) + for row, (start, stop) in it } if use_row_index: return { row: { - id_to_key[col]: val for col, val in zip(col_indices[start:stop], values[start:stop]) + id_to_key[col]: val + for col, val in zip(col_indices[start:stop], values[start:stop], strict=True) } for row, (start, stop) in it } if use_column_index: return { - id_to_key[row]: dict(zip(col_indices[start:stop], values[start:stop])) + id_to_key[row]: dict(zip(col_indices[start:stop], values[start:stop], strict=True)) for row, (start, stop) in it } return { id_to_key[row]: { - id_to_key[col]: val for col, val in zip(col_indices[start:stop], values[start:stop]) + id_to_key[col]: val + for col, val in zip(col_indices[start:stop], values[start:stop], strict=True) } for row, (start, stop) in it } @@ -239,9 +242,9 @@ def to_networkx(self, edge_attribute="weight"): rows = (id_to_key[row] for row in rows.tolist()) cols = (id_to_key[col] for col in cols.tolist()) if edge_attribute is None: - G.add_edges_from(zip(rows, cols)) + G.add_edges_from(zip(rows, cols, strict=True)) else: - G.add_weighted_edges_from(zip(rows, cols, vals), weight=edge_attribute) + G.add_weighted_edges_from(zip(rows, cols, vals, strict=True), weight=edge_attribute) # What else should we copy over? return G @@ -258,4 +261,4 @@ def renumber_key_to_id(self, indices): return {id_to_key[index]: i for i, index in enumerate(indices)} # Alternative (about the same performance) # keys = self.list_to_keys(indices) - # return dict(zip(keys, range(len(indices)))) + # return dict(zip(keys, range(len(indices)), strict=True)) diff --git a/graphblas_algorithms/interface.py b/graphblas_algorithms/interface.py index 1d8c283..c718371 100644 --- a/graphblas_algorithms/interface.py +++ b/graphblas_algorithms/interface.py @@ -281,31 +281,31 @@ def key(testpath): return (testname, frozenset({filename})) # Reasons to skip tests - multi_attributed = "unable to handle multi-attributed graphs" + # multi_attributed = "unable to handle multi-attributed graphs" multidigraph = "unable to handle MultiDiGraph" multigraph = "unable to handle MultiGraph" # Which tests to skip skip = { - key("test_mst.py:TestBoruvka.test_attributes"): multi_attributed, - key("test_mst.py:TestBoruvka.test_weight_attribute"): multi_attributed, + # key("test_mst.py:TestBoruvka.test_attributes"): multi_attributed, + # key("test_mst.py:TestBoruvka.test_weight_attribute"): multi_attributed, key("test_dense.py:TestFloyd.test_zero_weight"): multidigraph, key("test_dense_numpy.py:test_zero_weight"): multidigraph, key("test_weighted.py:TestBellmanFordAndGoldbergRadzik.test_multigraph"): multigraph, - key("test_binary.py:test_compose_multigraph"): multigraph, - key("test_binary.py:test_difference_multigraph_attributes"): multigraph, - key("test_binary.py:test_disjoint_union_multigraph"): multigraph, - key("test_binary.py:test_full_join_multigraph"): multigraph, - key("test_binary.py:test_intersection_multigraph_attributes"): multigraph, - key( - "test_binary.py:test_intersection_multigraph_attributes_node_set_different" - ): multigraph, - key("test_binary.py:test_symmetric_difference_multigraph"): multigraph, - key("test_binary.py:test_union_attributes"): multi_attributed, + # key("test_binary.py:test_compose_multigraph"): multigraph, + # key("test_binary.py:test_difference_multigraph_attributes"): multigraph, + # key("test_binary.py:test_disjoint_union_multigraph"): multigraph, + # key("test_binary.py:test_full_join_multigraph"): multigraph, + # key("test_binary.py:test_intersection_multigraph_attributes"): multigraph, + # key( + # "test_binary.py:test_intersection_multigraph_attributes_node_set_different" + # ): multigraph, + # key("test_binary.py:test_symmetric_difference_multigraph"): multigraph, + # key("test_binary.py:test_union_attributes"): multi_attributed, # TODO: move failing assertion from `test_union_and_compose` - key("test_binary.py:test_union_and_compose"): multi_attributed, - key("test_binary.py:test_union_multigraph"): multigraph, - key("test_vf2pp.py:test_custom_multigraph4_different_labels"): multigraph, + # key("test_binary.py:test_union_and_compose"): multi_attributed, + # key("test_binary.py:test_union_multigraph"): multigraph, + # key("test_vf2pp.py:test_custom_multigraph4_different_labels"): multigraph, } for item in items: kset = set(item.keywords) diff --git a/graphblas_algorithms/nxapi/boundary.py b/graphblas_algorithms/nxapi/boundary.py index 8907f09..662cfe4 100644 --- a/graphblas_algorithms/nxapi/boundary.py +++ b/graphblas_algorithms/nxapi/boundary.py @@ -29,15 +29,19 @@ def edge_boundary(G, nbunch1, nbunch2=None, data=False, keys=False, default=None (id_to_key[col] for col in cols), # Unsure about this; data argument may mean *all* edge attributes ({weight: val} for val in vals), + strict=True, ) else: it = zip( (id_to_key[row] for row in rows), (id_to_key[col] for col in cols), + strict=True, ) if is_multigraph: # Edge weights indicate number of times to repeat edges - it = itertools.chain.from_iterable(itertools.starmap(itertools.repeat, zip(it, vals))) + it = itertools.chain.from_iterable( + itertools.starmap(itertools.repeat, zip(it, vals, strict=True)) + ) return it diff --git a/pyproject.toml b/pyproject.toml index 6ed6386..b1625c6 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.9" +requires-python = ">=3.10" 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.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -134,7 +133,7 @@ dirty_template = "{tag}+{ccount}.g{sha}.dirty" [tool.black] line-length = 100 -target-version = ["py39", "py310", "py311", "py312"] +target-version = ["py310", "py311", "py312"] [tool.isort] sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] @@ -177,7 +176,7 @@ exclude_lines = [ [tool.ruff] # https://github.com/charliermarsh/ruff/ line-length = 100 -target-version = "py39" +target-version = "py310" unfixable = [ "F841" # unused-variable (Note: can leave useless expression) ] @@ -205,6 +204,7 @@ ignore = [ # "SIM401", # Use dict.get ... instead of if-else-block (Note: if-else better for coverage and sometimes clearer) # "TRY004", # Prefer `TypeError` exception for invalid type (Note: good advice, but not worth the nuisance) # "TRY200", # Use `raise from` to specify exception cause (Note: sometimes okay to raise original exception) + "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` (Note: using `|` seems to be slower) # Intentionally ignored "COM812", # Trailing comma missing diff --git a/scripts/scipy_impl.py b/scripts/scipy_impl.py index 06244ea..35815a6 100644 --- a/scripts/scipy_impl.py +++ b/scripts/scipy_impl.py @@ -50,7 +50,7 @@ def pagerank( err = np.absolute(x - xlast).sum() if err < N * tol: return x - # return dict(zip(nodelist, map(float, x))) + # return dict(zip(nodelist, map(float, x), strict=True)) raise nx.PowerIterationFailedConvergence(max_iter)