diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2476207c..4bbe8c32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,20 +10,16 @@ on: - cron: '17 3 * * 0' jobs: - flake8: - name: Flake8 + ruff: + name: Ruff runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - # matches compat target in setup.py - python-version: '3.8' + - uses: actions/setup-python@v5 - name: "Main Script" run: | - curl -L -O https://gitlab.tiker.net/inducer/ci-support/raw/main/prepare-and-run-flake8.sh - . ./prepare-and-run-flake8.sh "$(basename $GITHUB_REPOSITORY)" + pip install ruff + ruff check validate_cff: name: Validate CITATION.cff diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6726e284..7f23c645 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,10 +42,10 @@ Pytest without Numpy: Flake8: script: - - curl -L -O https://gitlab.tiker.net/inducer/ci-support/raw/main/prepare-and-run-flake8.sh - - . ./prepare-and-run-flake8.sh "$CI_PROJECT_NAME" + - pipx install ruff + - ruff check tags: - - python3 + - docker-runner except: - tags diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..b0b8bc94 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,97 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=63", +] + +[project] +name = "pytools" +version = "2024.1.6" +description = "A collection of tools for Python" +readme = "README.rst" +license = { text = "MIT" } +requires-python = "~=3.8" +authors = [ + { name = "Andreas Kloeckner", email = "inform@tiker.net" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Other Audience", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Scientific/Engineering :: Visualization", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities", +] +dependencies = [ + "platformdirs>=2.2.0", + "typing_extensions>=4.0; python_version<'3.11'", +] + +[project.optional-dependencies] +numpy = [ + "numpy>=1.6.0", +] + +test = [ + "mypy", + "pytest", + "ruff", +] + +[project.urls] +Homepage = "https://github.com/inducer/pytools/" +Documentation = "https://documen.tician.de/pytools/" + +[tool.setuptools.package-data] +pytools = [ + "py.typed", +] + +[tool.ruff] +target-version = "py38" +line-length = 85 + +preview = true +[tool.ruff.lint] +extend-select = [ + "B", # flake8-bugbear + "C", # flake8-comprehensions + "E", # pycodestyle + "F", # pyflakes + "I", # flake8-isort + "N", # pep8-naming + "NPY", # numpy + "Q", # flake8-quotes + "W", # pycodestyle +] +extend-ignore = [ + "C90", # McCabe complexity + "E221", # multiple spaces before operator + "E226", # missing whitespace around arithmetic operator + "E402", # module-level import not at top of file +] +[tool.ruff.lint.flake8-quotes] +docstring-quotes = "double" +inline-quotes = "double" +multiline-quotes = "double" + +[tool.ruff.lint.isort] +combine-as-imports = true + +known-local-folder = [ + "pytools", +] +lines-after-imports = 2 + +[tool.mypy] +ignore_missing_imports = true +warn_unused_ignores = true + diff --git a/pytools/__init__.py b/pytools/__init__.py index 2304462b..b09bf036 100644 --- a/pytools/__init__.py +++ b/pytools/__init__.py @@ -36,8 +36,24 @@ from functools import reduce, wraps from sys import intern from typing import ( - Any, Callable, ClassVar, Dict, Generic, Hashable, Iterable, Iterator, List, - Mapping, Optional, Sequence, Set, Tuple, Type, TypeVar, Union) + Any, + Callable, + ClassVar, + Dict, + Generic, + Hashable, + Iterable, + Iterator, + List, + Mapping, + Optional, + Sequence, + Set, + Tuple, + Type, + TypeVar, + Union, +) try: @@ -528,7 +544,8 @@ def __init__(self, value): def get(self): from warnings import warn - warn("Reference.get() is deprecated -- use ref.value instead") + warn("Reference.get() is deprecated -- use ref.value instead. " + "This will stop working in 2025.", stacklevel=2) return self.value def set(self, value): @@ -607,7 +624,7 @@ def one(iterable: Iterable[T]) -> T: try: v = next(it) except StopIteration: - raise ValueError("empty iterable passed to 'one()'") + raise ValueError("empty iterable passed to 'one()'") from None def no_more(): try: @@ -629,7 +646,7 @@ def is_single_valued( try: first_item = next(it) except StopIteration: - raise ValueError("empty iterable passed to 'single_valued()'") + raise ValueError("empty iterable passed to 'single_valued()'") from None for other_item in it: if not equality_pred(other_item, first_item): @@ -656,7 +673,7 @@ def single_valued( try: first_item = next(it) except StopIteration: - raise ValueError("empty iterable passed to 'single_valued()'") + raise ValueError("empty iterable passed to 'single_valued()'") from None def others_same(): for other_item in it: @@ -1241,7 +1258,7 @@ def argmin2(iterable, return_value=False): try: current_argmin, current_min = next(it) except StopIteration: - raise ValueError("argmin of empty iterable") + raise ValueError("argmin of empty iterable") from None for arg, item in it: if item < current_min: @@ -1259,7 +1276,7 @@ def argmax2(iterable, return_value=False): try: current_argmax, current_max = next(it) except StopIteration: - raise ValueError("argmax of empty iterable") + raise ValueError("argmax of empty iterable") from None for arg, item in it: if item > current_max: @@ -1326,7 +1343,7 @@ def average(iterable): s = next(it) count = 1 except StopIteration: - raise ValueError("empty average") + raise ValueError("empty average") from None for value in it: s = s + value @@ -1441,7 +1458,7 @@ def generate_decreasing_nonnegative_tuples_summing_to( yield () elif length == 1: if n <= max_value: - #print "MX", n, max_value + # print "MX", n, max_value yield (n,) else: return @@ -1450,7 +1467,7 @@ def generate_decreasing_nonnegative_tuples_summing_to( max_value = n for i in range(min_value, max_value+1): - #print "SIG", sig, i + # print "SIG", sig, i for remainder in generate_decreasing_nonnegative_tuples_summing_to( n-i, length-1, min_value, i): yield (i,) + remainder @@ -1502,7 +1519,7 @@ def generate_permutations(original): else: for perm_ in generate_permutations(original[1:]): for i in range(len(perm_)+1): - #nb str[0:1] works in both string and list contexts + # nb str[0:1] works in both string and list contexts yield perm_[:i] + original[0:1] + perm_[i:] @@ -1527,7 +1544,7 @@ def enumerate_basic_directions(dimensions): # {{{ graph algorithms -from pytools.graph import a_star as a_star_moved +from pytools.graph import a_star as a_star_moved # noqa: E402 a_star = MovedFunctionDeprecationWrapper(a_star_moved) @@ -1808,7 +1825,7 @@ def string_histogram( # pylint: disable=too-many-arguments,too-many-locals for value in iterable: if max_value is not None and value > max_value or value < bin_starts[0]: from warnings import warn - warn("string_histogram: out-of-bounds value ignored") + warn("string_histogram: out-of-bounds value ignored", stacklevel=2) else: bin_nr = bisect(bin_starts, value)-1 try: @@ -2425,7 +2442,7 @@ def find_git_revision(tree_root): # pylint: disable=too-many-locals assert retcode is not None if retcode != 0: from warnings import warn - warn("unable to find git revision") + warn("unable to find git revision", stacklevel=1) return None return git_rev @@ -2704,7 +2721,8 @@ def natsorted(iterable, key=None, reverse=False): .. versionadded:: 2020.1 """ if key is None: - key = lambda x: x + def key(x): + return x return sorted(iterable, key=lambda y: natorder(key(y)), reverse=reverse) # }}} diff --git a/pytools/debug.py b/pytools/debug.py index 71e08d2e..b4923f6a 100644 --- a/pytools/debug.py +++ b/pytools/debug.py @@ -51,7 +51,7 @@ def open_unique_debug_file(stem, extension=""): # {{{ refcount debugging ------------------------------------------------------ -class RefDebugQuit(Exception): +class RefDebugQuit(Exception): # noqa: N818 pass @@ -159,7 +159,7 @@ def setup_readline(): e = sys.exc_info()[1] from warnings import warn - warn(f"Error opening readline history file: {e}") + warn(f"Error opening readline history file: {e}", stacklevel=2) readline.parse_and_bind("tab: complete") diff --git a/pytools/graph.py b/pytools/graph.py index 1a024841..09db7aaf 100644 --- a/pytools/graph.py +++ b/pytools/graph.py @@ -65,8 +65,20 @@ """ from typing import ( - Any, Callable, Collection, Dict, Hashable, Iterator, List, Mapping, MutableSet, - Optional, Set, Tuple, TypeVar) + Any, + Callable, + Collection, + Dict, + Hashable, + Iterator, + List, + Mapping, + MutableSet, + Optional, + Set, + Tuple, + TypeVar, +) try: @@ -287,7 +299,7 @@ def compute_topological_order(graph: GraphT[NodeT], # {{{ compute nodes_to_num_predecessors - nodes_to_num_predecessors = {node: 0 for node in graph} + nodes_to_num_predecessors = dict.fromkeys(graph, 0) for node in graph: for child in graph[node]: @@ -437,10 +449,12 @@ def as_graphviz_dot(graph: GraphT[NodeT], from pytools.graphviz import dot_escape if node_labels is None: - node_labels = lambda x: str(x) + def node_labels(x): + return str(x) if edge_labels is None: - edge_labels = lambda x, y: "" + def edge_labels(x, y): + return "" node_to_id = {} diff --git a/pytools/persistent_dict.py b/pytools/persistent_dict.py index 89ef3a8a..ed4fce98 100644 --- a/pytools/persistent_dict.py +++ b/pytools/persistent_dict.py @@ -39,8 +39,18 @@ from dataclasses import fields as dc_fields, is_dataclass from enum import Enum from typing import ( - TYPE_CHECKING, Any, Callable, FrozenSet, Iterator, Mapping, Optional, Protocol, - Tuple, TypeVar, cast) + TYPE_CHECKING, + Any, + Callable, + FrozenSet, + Iterator, + Mapping, + Optional, + Protocol, + Tuple, + TypeVar, + cast, +) if TYPE_CHECKING: @@ -224,7 +234,7 @@ def rec(self, key_hash: Hash, key: Any) -> Hash: if not isinstance(key, type): try: # pylint:disable=protected-access - object.__setattr__(key, "_pytools_persistent_hash_digest", digest) + object.__setattr__(key, "_pytools_persistent_hash_digest", digest) except AttributeError: pass except TypeError: @@ -418,7 +428,7 @@ def __getattr__(name: str) -> Any: if name in ("NoSuchEntryInvalidKeyError", "NoSuchEntryInvalidContentsError"): from warnings import warn - warn(f"pytools.persistent_dict.{name} has been removed.") + warn(f"pytools.persistent_dict.{name} has been removed.", stacklevel=2) return NoSuchEntryError raise AttributeError(name) @@ -517,7 +527,8 @@ def _collision_check(self, key: K, stored_key: K) -> None: "that they're often indicative of a broken hash key " "implementation (that is not considering some elements " "relevant for equality comparison)", - CollisionWarning + CollisionWarning, + stacklevel=3 ) # This is here so we can step through equality comparison to @@ -551,7 +562,7 @@ def _exec_sql_fn(self, fn: Callable[[], T]) -> Optional[T]: if n % 20 == 0: from warnings import warn warn(f"PersistentDict: database '{self.filename}' busy, {n} " - "retries") + "retries", stacklevel=3) else: break @@ -696,12 +707,12 @@ def store(self, key: K, value: V, _skip_if_present: bool = False) -> None: if hasattr(e, "sqlite_errorcode"): if e.sqlite_errorcode == sqlite3.SQLITE_CONSTRAINT_PRIMARYKEY: raise ReadOnlyEntryError("WriteOncePersistentDict, " - "tried overwriting key") + "tried overwriting key") from e else: raise else: raise ReadOnlyEntryError("WriteOncePersistentDict, " - "tried overwriting key") + "tried overwriting key") from e def _fetch_uncached(self, keyhash: str) -> Tuple[K, V]: # This method is separate from fetch() to allow for LRU caching @@ -719,8 +730,8 @@ def fetch(self, key: K) -> V: try: stored_key, value = self._fetch(keyhash) - except KeyError: - raise NoSuchEntryError(key) + except KeyError as err: + raise NoSuchEntryError(key) from err else: self._collision_check(key, stored_key) return value diff --git a/pytools/prefork.py b/pytools/prefork.py index c2ed6d3c..aa76c6b8 100644 --- a/pytools/prefork.py +++ b/pytools/prefork.py @@ -23,7 +23,8 @@ def call(cmdline, cwd=None): try: return spcall(cmdline, cwd=cwd) except OSError as e: - raise ExecError("error invoking '{}': {}".format(" ".join(cmdline), e)) + raise ExecError( + "error invoking '{}': {}".format(" ".join(cmdline), e)) from e def call_async(self, cmdline, cwd=None): from subprocess import Popen @@ -36,7 +37,8 @@ def call_async(self, cmdline, cwd=None): return self.count except OSError as e: - raise ExecError("error invoking '{}': {}".format(" ".join(cmdline), e)) + raise ExecError( + "error invoking '{}': {}".format(" ".join(cmdline), e)) from e @staticmethod def call_capture_output(cmdline, cwd=None, error_on_nonzero=True): @@ -55,7 +57,8 @@ def call_capture_output(cmdline, cwd=None, error_on_nonzero=True): return popen.returncode, stdout_data, stderr_data except OSError as e: - raise ExecError("error invoking '{}': {}".format(" ".join(cmdline), e)) + raise ExecError( + "error invoking '{}': {}".format(" ".join(cmdline), e)) from e def wait(self, aid): proc = self.apids.pop(aid) diff --git a/pytools/py_codegen.py b/pytools/py_codegen.py index 4e082397..887497c0 100644 --- a/pytools/py_codegen.py +++ b/pytools/py_codegen.py @@ -25,7 +25,10 @@ from types import FunctionType, ModuleType from pytools.codegen import ( # noqa - CodeGenerator as CodeGeneratorBase, Indentation, remove_common_indentation) + CodeGenerator as CodeGeneratorBase, + Indentation, + remove_common_indentation, +) class PythonCodeGenerator(CodeGeneratorBase): diff --git a/pytools/tag.py b/pytools/tag.py index 7e4f32f8..2130e041 100644 --- a/pytools/tag.py +++ b/pytools/tag.py @@ -30,7 +30,15 @@ from dataclasses import dataclass from typing import ( # noqa: F401 - Any, FrozenSet, Iterable, Set, Tuple, Type, TypeVar, Union) + Any, + FrozenSet, + Iterable, + Set, + Tuple, + Type, + TypeVar, + Union, +) from pytools import memoize, memoize_method diff --git a/pytools/test/test_persistent_dict.py b/pytools/test/test_persistent_dict.py index 2f6812c3..b0e050ed 100644 --- a/pytools/test/test_persistent_dict.py +++ b/pytools/test/test_persistent_dict.py @@ -8,8 +8,14 @@ import pytest from pytools.persistent_dict import ( - CollisionWarning, KeyBuilder, NoSuchEntryCollisionError, NoSuchEntryError, - PersistentDict, ReadOnlyEntryError, WriteOncePersistentDict) + CollisionWarning, + KeyBuilder, + NoSuchEntryCollisionError, + NoSuchEntryError, + PersistentDict, + ReadOnlyEntryError, + WriteOncePersistentDict, +) from pytools.tag import Tag, tag_dataclass @@ -495,7 +501,7 @@ def test_ABC_hashing() -> None: # noqa: N802 keyb = KeyBuilder() - class MyABC(ABC): + class MyABC(ABC): # noqa: B024 pass assert keyb(MyABC) != keyb(ABC) diff --git a/pytools/test/test_pytools.py b/pytools/test/test_pytools.py index 9e393a78..8bcd9af8 100644 --- a/pytools/test/test_pytools.py +++ b/pytools/test/test_pytools.py @@ -23,6 +23,7 @@ import logging import sys +from typing import FrozenSet import pytest @@ -30,7 +31,6 @@ logger = logging.getLogger(__name__) -from typing import FrozenSet def test_memoize_method_clear(): @@ -223,8 +223,10 @@ def double_value(self): def test_spatial_btree(dims, do_plot=False): pytest.importorskip("numpy") import numpy as np + + rng = np.random.default_rng() nparticles = 2000 - x = -1 + 2*np.random.rand(dims, nparticles) + x = -1 + 2*rng.uniform(size=(dims, nparticles)) x = np.sign(x)*np.abs(x)**1.9 x = (1.4 + x) % 2 - 1 @@ -360,8 +362,6 @@ def test_eoc(): # {{{ test invalid inputs - import numpy as np - eoc = EOCRecorder() # scalar inputs are fine @@ -500,7 +500,12 @@ def vectorized_add(self, ary): def test_tag(): from pytools.tag import ( - NonUniqueTagError, Tag, Taggable, UniqueTag, check_tag_uniqueness) + NonUniqueTagError, + Tag, + Taggable, + UniqueTag, + check_tag_uniqueness, + ) # Need a subclass that defines the copy function in order to test. class TaggableWithCopy(Taggable): @@ -840,7 +845,7 @@ def test_record(): assert str(r) == "SimpleRecord(a=1, b=2, c=3, d=4, e=5)" with pytest.raises(AttributeError): - r.ff + r.ff # noqa: B018 # Test pickling import pickle diff --git a/pytools/version.py b/pytools/version.py index 87424d5a..67a75117 100644 --- a/pytools/version.py +++ b/pytools/version.py @@ -1,3 +1,9 @@ -VERSION = (2024, 1, 6) -VERSION_STATUS = "" -VERSION_TEXT = ".".join(str(x) for x in VERSION) + VERSION_STATUS +import re +from importlib import metadata + + +VERSION_TEXT = metadata.version("pytools") +_match = re.match("^([0-9.]+)([a-z0-9]*?)$", VERSION_TEXT) +assert _match is not None +VERSION_STATUS = _match.group(2) +VERSION = tuple(int(nr) for nr in _match.group(1).split(".")) diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 6f288f8e..00000000 --- a/setup.cfg +++ /dev/null @@ -1,23 +0,0 @@ -[flake8] -ignore = E126,E127,E128,E123,E226,E241,E242,E265,E402,W503,E731,N818 -max-line-length=85 - -inline-quotes = " -docstring-quotes = " -multiline-quotes = """ - -# enable-flake8-bugbear -# enable-isort - -[isort] -line_length = 85 -lines_after_imports = 2 -combine_as_imports = True -multi_line_output = 4 - -[wheel] -universal = 1 - -[mypy] -ignore_missing_imports = True -warn_unused_ignores = true diff --git a/setup.py b/setup.py deleted file mode 100644 index f082237e..00000000 --- a/setup.py +++ /dev/null @@ -1,53 +0,0 @@ -#! /usr/bin/env python - -from setuptools import find_packages, setup - - -ver_dic = {} -version_file = open("pytools/version.py") -try: - version_file_contents = version_file.read() -finally: - version_file.close() - -exec(compile(version_file_contents, "pytools/version.py", "exec"), ver_dic) - -setup(name="pytools", - version=ver_dic["VERSION_TEXT"], - description="A collection of tools for Python", - long_description=open("README.rst").read(), - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "Intended Audience :: Other Audience", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", - "Natural Language :: English", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Information Analysis", - "Topic :: Scientific/Engineering :: Mathematics", - "Topic :: Scientific/Engineering :: Visualization", - "Topic :: Software Development :: Libraries", - "Topic :: Utilities", - ], - - python_requires="~=3.8", - - install_requires=[ - "platformdirs>=2.2.0", - "typing_extensions>=4.0; python_version<'3.11'", - ], - - package_data={"pytools": ["py.typed"]}, - - extras_require={ - "numpy": ["numpy>=1.6.0"], - }, - - author="Andreas Kloeckner", - url="http://pypi.python.org/pypi/pytools", - author_email="inform@tiker.net", - license="MIT", - packages=find_packages())