diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04a837493..df37d4461 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -71,7 +71,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.247 + rev: v0.0.249 hooks: - id: ruff - repo: https://github.com/sphinx-contrib/sphinx-lint diff --git a/docs/conf.py b/docs/conf.py index 60020c247..ddd360326 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,10 +10,10 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -import os import sys +from pathlib import Path -sys.path.insert(0, os.path.abspath("..")) +sys.path.insert(0, str(Path("..").resolve())) # -- Project information ----------------------------------------------------- diff --git a/graphblas/__init__.py b/graphblas/__init__.py index 879571eab..87311599c 100644 --- a/graphblas/__init__.py +++ b/graphblas/__init__.py @@ -20,14 +20,14 @@ def __reduce__(self): def get_config(): - import os + from pathlib import Path import donfig import yaml - filename = os.path.join(os.path.dirname(__file__), "graphblas.yaml") config = donfig.Config("graphblas") - with open(filename) as f: + path = Path(__file__).parent / "graphblas.yaml" + with path.open() as f: defaults = yaml.safe_load(f) config.update_defaults(defaults) return config diff --git a/graphblas/core/automethods.py b/graphblas/core/automethods.py index d370469e7..98dc61137 100644 --- a/graphblas/core/automethods.py +++ b/graphblas/core/automethods.py @@ -339,7 +339,7 @@ def __ixor__(self, other): # End auto-generated code def _main(): - import os + from pathlib import Path from .utils import _autogenerate_code @@ -460,7 +460,7 @@ def _main(): f' raise TypeError(f"{name!r} not supported for {{type(self).__name__}}")\n\n' ) - _autogenerate_code(__file__, "\n".join(lines)) + _autogenerate_code(Path(__file__), "\n".join(lines)) # Copy to scalar.py and infix.py lines = [] @@ -481,16 +481,16 @@ def _main(): continue lines.append(f" {name} = automethods.{name}") - thisdir = os.path.dirname(__file__) + thisdir = Path(__file__).parent infix_exclude = {"_get_value"} def get_name(line): return line.strip().split(" ", 1)[0] text = "\n".join(lines) + "\n " - _autogenerate_code(os.path.join(thisdir, "scalar.py"), text, "Scalar") + _autogenerate_code(thisdir / "scalar.py", text, "Scalar") text = "\n".join(line for line in lines if get_name(line) not in infix_exclude) + "\n " - _autogenerate_code(os.path.join(thisdir, "infix.py"), text, "Scalar") + _autogenerate_code(thisdir / "infix.py", text, "Scalar") # Copy to vector.py and infix.py lines = [] @@ -519,9 +519,9 @@ def get_name(line): lines.append(f" {name} = automethods.{name}") text = "\n".join(lines) + "\n " - _autogenerate_code(os.path.join(thisdir, "vector.py"), text, "Vector") + _autogenerate_code(thisdir / "vector.py", text, "Vector") text = "\n".join(line for line in lines if get_name(line) not in infix_exclude) + "\n " - _autogenerate_code(os.path.join(thisdir, "infix.py"), text, "Vector") + _autogenerate_code(thisdir / "infix.py", text, "Vector") # Copy to matrix.py and infix.py lines = [] @@ -550,9 +550,9 @@ def get_name(line): lines.append(f" {name} = automethods.{name}") text = "\n".join(lines) + "\n " - _autogenerate_code(os.path.join(thisdir, "matrix.py"), text, "Matrix") + _autogenerate_code(thisdir / "matrix.py", text, "Matrix") text = "\n".join(line for line in lines if get_name(line) not in infix_exclude) + "\n " - _autogenerate_code(os.path.join(thisdir, "infix.py"), text, "Matrix") + _autogenerate_code(thisdir / "infix.py", text, "Matrix") if __name__ == "__main__": diff --git a/graphblas/core/infixmethods.py b/graphblas/core/infixmethods.py index d3b0dd5e4..71201ad9c 100644 --- a/graphblas/core/infixmethods.py +++ b/graphblas/core/infixmethods.py @@ -426,9 +426,11 @@ def _main(): " setattr(VectorIndexExpr, name, val)\n" " setattr(MatrixIndexExpr, name, val)\n" ) + from pathlib import Path + from .utils import _autogenerate_code - _autogenerate_code(__file__, "\n".join(lines)) + _autogenerate_code(Path(__file__), "\n".join(lines)) if __name__ == "__main__": diff --git a/graphblas/core/utils.py b/graphblas/core/utils.py index e0df29db4..0beeb4a2a 100644 --- a/graphblas/core/utils.py +++ b/graphblas/core/utils.py @@ -352,14 +352,14 @@ def __init__(self, matrices, exc_arg=None, *, name): def _autogenerate_code( - filename, + filepath, text, specializer=None, begin="# Begin auto-generated code", end="# End auto-generated code", ): """Super low-tech auto-code generation used by automethods.py and infixmethods.py.""" - with open(filename) as f: # pragma: no branch (flaky) + with filepath.open() as f: # pragma: no branch (flaky) orig_text = f.read() if specializer: begin = f"{begin}: {specializer}" @@ -379,11 +379,11 @@ def _autogenerate_code( new_text = orig_text for start, stop in reversed(boundaries): new_text = f"{new_text[:start]}{begin}{text}{new_text[stop:]}" - with open(filename, "w") as f: # pragma: no branch (flaky) + with filepath.open("w") as f: # pragma: no branch (flaky) f.write(new_text) import subprocess try: - subprocess.check_call(["black", filename]) + subprocess.check_call(["black", filepath]) except FileNotFoundError: # pragma: no cover (safety) pass # It's okay if `black` isn't installed; pre-commit hooks will do linting diff --git a/graphblas/tests/conftest.py b/graphblas/tests/conftest.py index cd66efa6f..24aba085f 100644 --- a/graphblas/tests/conftest.py +++ b/graphblas/tests/conftest.py @@ -1,6 +1,7 @@ import atexit import functools import itertools +from pathlib import Path import numpy as np import pytest @@ -12,26 +13,27 @@ def pytest_configure(config): + rng = np.random.default_rng() randomly = config.getoption("--randomly", False) backend = config.getoption("--backend", None) if backend is None: if randomly: - backend = "suitesparse" if np.random.rand() < 0.5 else "suitesparse-vanilla" + backend = "suitesparse" if rng.random() < 0.5 else "suitesparse-vanilla" else: backend = "suitesparse" blocking = config.getoption("--blocking", True) if blocking is None: # pragma: no branch - blocking = np.random.rand() < 0.5 if randomly else True + blocking = rng.random() < 0.5 if randomly else True record = config.getoption("--record", False) if record is None: # pragma: no branch - record = np.random.rand() < 0.5 if randomly else False + record = rng.random() < 0.5 if randomly else False mapnumpy = config.getoption("--mapnumpy", False) if mapnumpy is None: - mapnumpy = np.random.rand() < 0.5 if randomly else False + mapnumpy = rng.random() < 0.5 if randomly else False runslow = config.getoption("--runslow", False) if runslow is None: # Add a small amount of randomization to be safer - runslow = np.random.rand() < 0.05 if randomly else False + runslow = rng.random() < 0.05 if randomly else False config.runslow = runslow gb.config.set(autocompute=False, mapnumpy=mapnumpy) @@ -46,7 +48,7 @@ def pytest_configure(config): rec.start() def save_records(): - with open("record.txt", "w") as f: # pragma: no cover + with Path("record.txt").open("w") as f: # pragma: no cover f.write("\n".join(rec.data)) # I'm sure there's a `pytest` way to do this... @@ -83,7 +85,7 @@ def pytest_runtest_setup(item): pytest.skip("need --runslow option to run") -@pytest.fixture(autouse=True, scope="function") +@pytest.fixture(autouse=True) def _reset_name_counters(): """Reset automatic names for each test for easier comparison of record.txt.""" gb.Matrix._name_counter = itertools.count() diff --git a/graphblas/tests/test_dtype.py b/graphblas/tests/test_dtype.py index 59288a096..1ed0c777b 100644 --- a/graphblas/tests/test_dtype.py +++ b/graphblas/tests/test_dtype.py @@ -193,7 +193,8 @@ def test_bad_register(): def test_auto_register(): - n = np.random.randint(10, 64) + rng = np.random.default_rng() + n = rng.integers(10, 64, endpoint=True) np_type = np.dtype(f"({n},)int16") assert lookup_dtype(np_type).np_type == np_type diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 92d3fad13..40676f71a 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -3794,13 +3794,14 @@ def get_expected(row_offset, col_offset, nrows, ncols, is_transposed): def test_to_coo_sort(): # How can we get a matrix to a jumbled state in SS so that export won't be sorted? N = 1000000 - r = np.unique(np.random.randint(N, size=100)) - c = np.unique(np.random.randint(N, size=100)) + rng = np.random.default_rng() + r = np.unique(rng.integers(N, size=100)) + c = np.unique(rng.integers(N, size=100)) r = r[: c.size].copy() # make sure same length c = c[: r.size].copy() expected_rows = r.copy() - np.random.shuffle(r) - np.random.shuffle(c) + rng.shuffle(r) + rng.shuffle(c) A = Matrix.from_coo(r, c, r, nrows=N, ncols=N) rows, cols, values = A.to_coo(sort=False) A = Matrix.from_coo(r, c, r, nrows=N, ncols=N) diff --git a/graphblas/tests/test_pickle.py b/graphblas/tests/test_pickle.py index f1dae22fa..de2d9cfda 100644 --- a/graphblas/tests/test_pickle.py +++ b/graphblas/tests/test_pickle.py @@ -1,5 +1,5 @@ -import os import pickle +from pathlib import Path import numpy as np import pytest @@ -38,12 +38,12 @@ def extra(): @pytest.mark.slow def test_deserialize(extra): - thisdir = os.path.dirname(__file__) - with open(os.path.join(thisdir, f"pickle1{extra}.pkl"), "rb") as f: + path = Path(__file__).parent / f"pickle1{extra}.pkl" + with path.open("rb") as f: d = pickle.load(f) check_values(d) # Again! - with open(os.path.join(thisdir, f"pickle1{extra}.pkl"), "rb") as f: + with path.open("rb") as f: d = pickle.load(f) check_values(d) @@ -287,11 +287,11 @@ def test_serialize_parameterized(): @pytest.mark.slow def test_deserialize_parameterized(extra): - thisdir = os.path.dirname(__file__) - with open(os.path.join(thisdir, f"pickle2{extra}.pkl"), "rb") as f: + path = Path(__file__).parent / f"pickle2{extra}.pkl" + with path.open("rb") as f: pickle.load(f) # TODO: check results # Again! - with open(os.path.join(thisdir, f"pickle2{extra}.pkl"), "rb") as f: + with path.open("rb") as f: pickle.load(f) # TODO: check results @@ -306,8 +306,8 @@ def test_udt(extra): assert udt2._is_anonymous assert pickle.loads(pickle.dumps(udt2)).np_type == udt2.np_type - thisdir = os.path.dirname(__file__) - with open(os.path.join(thisdir, f"pickle3{extra}.pkl"), "rb") as f: + path = Path(__file__).parent / f"pickle3{extra}.pkl" + with path.open("rb") as f: d = pickle.load(f) udt3 = d["PickledUDT"] v = d["v"] diff --git a/graphblas/tests/test_prefix_scan.py b/graphblas/tests/test_prefix_scan.py index ea169a632..674a20267 100644 --- a/graphblas/tests/test_prefix_scan.py +++ b/graphblas/tests/test_prefix_scan.py @@ -14,7 +14,8 @@ @pytest.mark.parametrize("do_random", [False, True]) def test_scan_matrix(method, length, do_random): if do_random: - A = np.random.randint(10, size=2 * length).reshape((2, length)) + rng = np.random.default_rng() + A = rng.integers(10, size=2 * length).reshape((2, length)) mask = (A % 2).astype(bool) A[~mask] = 0 M = Matrix.ss.import_bitmapr(values=A, bitmap=mask, name="A") @@ -44,7 +45,8 @@ def test_scan_matrix(method, length, do_random): @pytest.mark.parametrize("do_random", [False, True]) def test_scan_vector(length, do_random): if do_random: - a = np.random.randint(10, size=length) + rng = np.random.default_rng() + a = rng.integers(10, size=length) mask = (a % 2).astype(bool) a[~mask] = 0 v = Vector.ss.import_bitmap(values=a, bitmap=mask) diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index 0fda52508..8505313e4 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -2347,9 +2347,10 @@ def get_expected(offset, size): def test_to_coo_sort(): # How can we get a vector to a jumbled state in SS so that export won't be sorted? N = 1000000 - a = np.unique(np.random.randint(N, size=100)) + rng = np.random.default_rng() + a = np.unique(rng.integers(N, size=100)) expected = a.copy() - np.random.shuffle(a) + rng.shuffle(a) v = Vector.from_coo(a, a, size=N) indices, values = v.to_coo(sort=False) v = Vector.from_coo(a, a, size=N) diff --git a/pyproject.toml b/pyproject.toml index 7dea49e98..083fd8656 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,12 @@ keywords = [ "matrix", "lagraph", "suitesparse", + "Networks", + "Graph Theory", + "Mathematics", + "network", + "discrete mathematics", + "math", ] classifiers = [ "Development Status :: 5 - Production/Stable", @@ -44,8 +50,9 @@ classifiers = [ "Intended Audience :: Other Audience", "Intended Audience :: Science/Research", "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Mathematics", "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ "suitesparse-graphblas >=7.4.0.0, <7.5", @@ -150,7 +157,6 @@ filterwarnings = [ branch = true source = ["graphblas"] omit = [ - "graphblas/_version.py", "graphblas/viz.py", # TODO: test and get coverage for viz.py ] @@ -185,7 +191,7 @@ select = [ "UP", # pyupgrade "YTT", # flake8-2020 # "ANN", # flake8-annotations (We don't use annotations yet) - "S", # x. bandit + "S", # bandit # "BLE", # flake8-blind-except (Maybe consider) # "FBT", # flake8-boolean-trap (Why?) "B", # flake8-bugbear @@ -195,7 +201,7 @@ select = [ "DTZ", # flake8-datetimez "T10", # flake8-debugger # "DJ", # flake8-django (We don't use django) - # "EM", # xx. flake8-errmsg (Perhaps nicer, but too much work) + # "EM", # flake8-errmsg (Perhaps nicer, but too much work) "EXE", # flake8-executable "ISC", # flake8-implicit-str-concat # "ICN", # flake8-import-conventions (Doesn't allow "_" prefix such as `_np`) @@ -205,17 +211,17 @@ select = [ "T20", # flake8-print # "PYI", # flake8-pyi (We don't have stub files yet) "PT", # flake8-pytest-style - "Q", # xxx. flake8-quotes + "Q", # flake8-quotes "RSE", # flake8-raise "RET", # flake8-return # "SLF", # flake8-self (We can use our own private variables--sheesh!) "SIM", # flake8-simplify # "TID", # flake8-tidy-imports (Rely on isort and our own judgement) - "TCH", # flake8-type-checking + # "TCH", # flake8-type-checking (Note: figure out type checking later) # "ARG", # flake8-unused-arguments (Sometimes helpful, but too strict) - # "PTH", # flake8-use-pathlib (Is there a strong argument for this?) + "PTH", # flake8-use-pathlib (Often better, but not always) # "ERA", # eradicate (We like code in comments!) - # "PD", # xl. pandas-vet (Intended for scripts that use pandas, not libraries) + # "PD", # pandas-vet (Intended for scripts that use pandas, not libraries) "PGH", # pygrep-hooks "PL", # pylint "PLC", # pylint Convention @@ -225,6 +231,7 @@ select = [ "TRY", # tryceratops "NPY", # NumPy-specific rules "RUF", # ruff-specific rules + "ALL", # Try new categories by default (making the above list unnecessary) ] external = [ # noqa codes that ruff doesn't know about: https://github.com/charliermarsh/ruff#external @@ -237,15 +244,15 @@ ignore = [ "D103", # Missing docstring in public function "D104", # Missing docstring in public package "D105", # Missing docstring in magic method - "D107", # Missing docstring in `__init__` + # "D107", # Missing docstring in `__init__` "D205", # 1 blank line required between summary line and description - "D212", # Multi-line docstring summary should start at the first line - "D213", # Multi-line docstring summary should start at the second line "D401", # First line of docstring should be in imperative mood: - "D417", # Missing argument description in the docstring: + # "D417", # Missing argument description in the docstring: "PLE0605", # Invalid format for `__all__`, must be `tuple` or `list` (Note: broken in v0.0.237) # Maybe consider + # "SIM300", # Yoda conditions are discouraged, use ... instead (Note: we're not this picky) + # "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) @@ -264,7 +271,6 @@ ignore = [ "PLR0913", # Too many arguments to function call "PLR0915", # Too many statements "PLR2004", # Magic number used in comparison, consider replacing magic with a constant variable - "PT003", # `scope='function'` is implied in `@pytest.fixture()` (Note: no harm in being explicit) "RET502", # Do not implicitly `return None` in function able to return non-`None` value "RET503", # Missing explicit `return` at the end of function able to return non-`None` value "RET504", # Unnecessary variable assignment before `return` statement @@ -273,19 +279,37 @@ ignore = [ "SIM102", # Use a single `if` statement instead of nested `if` statements (Note: often necessary) "SIM105", # Use contextlib.suppress(...) instead of try-except-pass (Note: try-except-pass is much faster) "SIM108", # Use ternary operator ... instead of if-else-block (Note: if-else better for coverage and sometimes clearer) - "SIM300", # Yoda conditions are discouraged, use ... instead (Note: we're not this picky) - # "SIM401", # Use dict.get ... instead of if-else-block (Note: if-else better for coverage and sometimes clearer) "TRY003", # Avoid specifying long messages outside the exception class (Note: why?) + + # Ignored categories + "C90", # mccabe (Too strict, but maybe we should make things less complex) + "I", # isort (Should we replace `isort` with this?) + "ANN", # flake8-annotations (We don't use annotations yet) + "BLE", # flake8-blind-except (Maybe consider) + "FBT", # flake8-boolean-trap (Why?) + "DJ", # flake8-django (We don't use django) + "EM", # flake8-errmsg (Perhaps nicer, but too much work) + "ICN", # flake8-import-conventions (Doesn't allow "_" prefix such as `_np`) + "PYI", # flake8-pyi (We don't have stub files yet) + "SLF", # flake8-self (We can use our own private variables--sheesh!) + "TID", # flake8-tidy-imports (Rely on isort and our own judgement) + "TCH", # flake8-type-checking (Note: figure out type checking later) + "ARG", # flake8-unused-arguments (Sometimes helpful, but too strict) + "ERA", # eradicate (We like code in comments!) + "PD", # pandas-vet (Intended for scripts that use pandas, not libraries) ] + [tool.ruff.per-file-ignores] -"graphblas/core/operator.py" = ["S102"] -"graphblas/tests/*py" = ["S101", "T201", "D103", "D100"] -"graphblas/tests/test_dtype.py" = ["UP003"] -"graphblas/tests/test_formatting.py" = ["E501"] -"graphblas/**/__init__.py" = ["F401"] -"scripts/*.py" = ["INP001"] -"scripts/create_pickle.py" = ["F403", "F405"] -"docs/*.py" = ["INP001"] +"graphblas/core/operator.py" = ["S102"] # exec is used for UDF +"graphblas/core/ss/matrix.py" = ["NPY002"] # numba doesn't support rng generator yet +"graphblas/core/ss/vector.py" = ["NPY002"] # numba doesn't support rng generator yet +"graphblas/ss/_core.py" = ["N999"] # We want _core.py to be underscopre +"graphblas/tests/*py" = ["S101", "T201", "D103", "D100", "SIM300"] # Allow assert, print, no docstring, and yoda +"graphblas/tests/test_formatting.py" = ["E501"] # Allow long lines +"graphblas/**/__init__.py" = ["F401"] # Allow unused imports (w/o defining `__all__`) +"scripts/*.py" = ["INP001"] # Not a package +"scripts/create_pickle.py" = ["F403", "F405"] # Allow `from foo import *` +"docs/*.py" = ["INP001"] # Not a package [tool.ruff.flake8-builtins] builtins-ignorelist = ["copyright", "format", "min", "max"] diff --git a/scripts/create_pickle.py b/scripts/create_pickle.py index 2af759f71..9ee672c41 100755 --- a/scripts/create_pickle.py +++ b/scripts/create_pickle.py @@ -5,14 +5,14 @@ Python version is used to create them. """ import argparse -import os import pickle +from pathlib import PurePath import graphblas as gb from graphblas.tests.test_pickle import * -def pickle1(filename): +def pickle1(filepath): suitesparse = gb.backend == "suitesparse" v = gb.Vector.from_coo([1], 2) @@ -29,7 +29,7 @@ def pickle1(filename): semiring_anon = gb.core.operator.Semiring.register_anonymous(monoid_anon, binary_anon) d = { "scalar": gb.Scalar.from_value(2), - "empty_scalar": gb.Scalar.new(bool), + "empty_scalar": gb.Scalar(bool), "vector": v, "matrix": gb.Matrix.from_coo([2], [3], 4), "matrix.T": gb.Matrix.from_coo([3], [4], 5).T, @@ -74,11 +74,11 @@ def pickle1(filename): } if suitesparse: d["agg.ss.first[int]"] = gb.agg.ss.first[int] - with open(filename, "wb") as f: + with filepath.open("wb") as f: pickle.dump(d, f) -def pickle2(filename): +def pickle2(filepath): unary_pickle = gb.core.operator.UnaryOp.register_new( "unary_pickle_par", unarypickle_par, parameterized=True ) @@ -118,22 +118,22 @@ def pickle2(filename): "semiring_pickle(0)": semiring_pickle(0), "unary_pickle(0)[UINT16]": unary_pickle(0)["UINT16"], } - with open(filename, "wb") as f: + with filepath.open("wb") as f: pickle.dump(d, f) -def pickle3(filename): +def pickle3(filepath): record_dtype = np.dtype([("a", np.bool_), ("b", np.int32)], align=True) udt = gb.dtypes.register_new("PickledUDT", record_dtype) np_dtype = np.dtype("(2,)uint32") udt2 = gb.dtypes.register_anonymous(np_dtype, "pickled_subdtype") - v = gb.Vector.new(udt, size=2) + v = gb.Vector(udt, size=2) v[0] = (False, 1) v[1] = (True, 3) - A = gb.Matrix.new(udt2, nrows=1, ncols=2) + A = gb.Matrix(udt2, nrows=1, ncols=2) A[0, 0] = (1, 2) A[0, 1] = (3, 4) d = { @@ -143,7 +143,7 @@ def pickle3(filename): "A": A, "any[udt]": gb.binary.any[udt], } - with open(filename, "wb") as f: + with filepath.open("wb") as f: pickle.dump(d, f) @@ -158,7 +158,7 @@ def pickle3(filename): extra = "-vanilla" else: extra = "" - basedir = os.path.dirname(gb.tests.__file__) - pickle1(os.path.join(basedir, f"pickle1{extra}.pkl")) - pickle2(os.path.join(basedir, f"pickle2{extra}.pkl")) - pickle3(os.path.join(basedir, f"pickle3{extra}.pkl")) + path = PurePath(gb.tests.__file__).parent + pickle1(path / f"pickle1{extra}.pkl") + pickle2(path / f"pickle2{extra}.pkl") + pickle3(path / f"pickle3{extra}.pkl")