From 72d26b59990a58c5eb1bcb808585c5c6313f4ce9 Mon Sep 17 00:00:00 2001
From: Andreas Kloeckner <inform@tiker.net>
Date: Mon, 1 Jul 2024 14:08:37 -0500
Subject: [PATCH] Switch to pyproject, ruff, fix ruff-flagged issues

---
 .github/workflows/ci.yml             | 14 ++--
 .gitlab-ci.yml                       |  6 +-
 pyproject.toml                       | 97 ++++++++++++++++++++++++++++
 pytools/__init__.py                  | 50 +++++++++-----
 pytools/debug.py                     |  4 +-
 pytools/graph.py                     | 22 +++++--
 pytools/persistent_dict.py           | 31 ++++++---
 pytools/prefork.py                   |  9 ++-
 pytools/py_codegen.py                |  5 +-
 pytools/tag.py                       | 10 ++-
 pytools/test/test_persistent_dict.py | 12 +++-
 pytools/test/test_pytools.py         | 17 +++--
 pytools/version.py                   | 12 +++-
 setup.cfg                            | 23 -------
 setup.py                             | 53 ---------------
 15 files changed, 228 insertions(+), 137 deletions(-)
 create mode 100644 pyproject.toml
 delete mode 100644 setup.cfg
 delete mode 100644 setup.py

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2476207..4bbe8c3 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 6726e28..7f23c64 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 0000000..b0b8bc9
--- /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 2304462..b09bf03 100644
--- a/pytools/__init__.py
+++ b/pytools/__init__.py
@@ -36,8 +36,24 @@ import sys
 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 @@ class Reference:
 
     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 71e08d2..b4923f6 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 9fd6f26..09db7aa 100644
--- a/pytools/graph.py
+++ b/pytools/graph.py
@@ -65,8 +65,20 @@ Type Variables Used
 """
 
 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:
@@ -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 89ef3a8..ed4fce9 100644
--- a/pytools/persistent_dict.py
+++ b/pytools/persistent_dict.py
@@ -39,8 +39,18 @@ import sys
 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 @@ class KeyBuilder:
         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 @@ class _PersistentDictBase(Mapping[K, V]):
                     "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 @@ class _PersistentDictBase(Mapping[K, V]):
                 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 @@ class WriteOncePersistentDict(_PersistentDictBase[K, V]):
                 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 @@ class WriteOncePersistentDict(_PersistentDictBase[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 c2ed6d3..aa76c6b 100644
--- a/pytools/prefork.py
+++ b/pytools/prefork.py
@@ -23,7 +23,8 @@ class DirectForker:
         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 @@ class DirectForker:
 
             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 @@ class DirectForker:
 
             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 4e08239..887497c 100644
--- a/pytools/py_codegen.py
+++ b/pytools/py_codegen.py
@@ -25,7 +25,10 @@ from importlib.util import MAGIC_NUMBER as BYTECODE_VERSION
 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 7e4f32f..2130e04 100644
--- a/pytools/tag.py
+++ b/pytools/tag.py
@@ -30,7 +30,15 @@ Internal stuff that is only here because the documentation tool wants it
 
 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 2f6812c..b0e050e 100644
--- a/pytools/test/test_persistent_dict.py
+++ b/pytools/test/test_persistent_dict.py
@@ -8,8 +8,14 @@ from typing import Any, Dict
 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 9e393a7..8bcd9af 100644
--- a/pytools/test/test_pytools.py
+++ b/pytools/test/test_pytools.py
@@ -23,6 +23,7 @@ THE SOFTWARE.
 
 import logging
 import sys
+from typing import FrozenSet
 
 import pytest
 
@@ -30,7 +31,6 @@ from pytools import Record
 
 
 logger = logging.getLogger(__name__)
-from typing import FrozenSet
 
 
 def test_memoize_method_clear():
@@ -223,8 +223,10 @@ def test_memoize_frozen() -> None:
 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 test_obj_array_vectorize(c=1):
 
 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 87424d5..67a7511 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 6f288f8..0000000
--- 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 f082237..0000000
--- 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())
-- 
GitLab