Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-pytools for openSUSE:Factory 
checked in at 2021-04-19 21:05:51
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pytools (Old)
 and      /work/SRC/openSUSE:Factory/.python-pytools.new.12324 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-pytools"

Mon Apr 19 21:05:51 2021 rev:13 rq:886512 version:2021.2.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pytools/python-pytools.changes    
2021-01-25 18:23:55.632455402 +0100
+++ /work/SRC/openSUSE:Factory/.python-pytools.new.12324/python-pytools.changes 
2021-04-19 21:06:09.492043661 +0200
@@ -1,0 +2,13 @@
+Mon Apr 19 02:01:33 UTC 2021 - Steve Kowalik <steven.kowa...@suse.com>
+
+- Update to 2021.2.3:
+  * Support pytools.tag in persistent_dict
+  * Add a backport of pkgutil.resolve_name
+  * Add persistent_dict.KeyBuilder.new_hash for hash alg customization
+  * Use unordered_hash in KeyBuilder hashing frozenset
+  * Drop dependency on, included obsolete copy of 'decorator' pypi module
+  * make obj_array_vectorize work on class methods 
+- Don't build for Python 3.6, due to no NumPy.
+- Remove decorator from {Build,}Requires
+
+-------------------------------------------------------------------

Old:
----
  pytools-2021.1.tar.gz

New:
----
  pytools-2021.2.3.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-pytools.spec ++++++
--- /var/tmp/diff_new_pack.GrEYwn/_old  2021-04-19 21:06:10.044044488 +0200
+++ /var/tmp/diff_new_pack.GrEYwn/_new  2021-04-19 21:06:10.044044488 +0200
@@ -18,17 +18,16 @@
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 %define skip_python2 1
+%define skip_python36 1
 Name:           python-pytools
-Version:        2021.1
+Version:        2021.2.3
 Release:        0
 Summary:        A collection of tools for Python
 License:        MIT
-Group:          Development/Languages/Python
 URL:            https://pypi.python.org/pypi/pytools
 Source0:        
https://files.pythonhosted.org/packages/source/p/pytools/pytools-%{version}.tar.gz
 BuildRequires:  %{python_module appdirs >= 1.4.0}
 BuildRequires:  %{python_module base}
-BuildRequires:  %{python_module decorator >= 3.2.0}
 BuildRequires:  %{python_module numpy >= 1.6.0}
 BuildRequires:  %{python_module pytest}
 BuildRequires:  %{python_module setuptools}
@@ -37,7 +36,6 @@
 BuildRequires:  ( python3-dataclasses >= 0.7 if python3-base <= 3.6 )
 BuildRequires:  ( python36-dataclasses >= 0.7 if python36-base )
 Requires:       python-appdirs >= 1.4.0
-Requires:       python-decorator >= 3.2.0
 Requires:       python-numpy >= 1.6.0
 %if %{python_version_nodots} <= 36
 Requires:       python-dataclasses >= 0.7

++++++ pytools-2021.1.tar.gz -> pytools-2021.2.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/PKG-INFO new/pytools-2021.2.3/PKG-INFO
--- old/pytools-2021.1/PKG-INFO 2021-01-09 20:56:50.842988300 +0100
+++ new/pytools-2021.2.3/PKG-INFO       2021-04-05 20:37:03.446051100 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: pytools
-Version: 2021.1
+Version: 2021.2.3
 Summary: A collection of tools for Python
 Home-page: http://pypi.python.org/pypi/pytools
 Author: Andreas Kloeckner
@@ -9,12 +9,12 @@
 Description: Pytools: Lots of Little Utilities
         =================================
         
-        .. image:: 
https://gitlab.tiker.net/inducer/pytools/badges/master/pipeline.svg
+        .. image:: 
https://gitlab.tiker.net/inducer/pytools/badges/main/pipeline.svg
             :alt: Gitlab Build Status
-            :target: https://gitlab.tiker.net/inducer/pytools/commits/master
-        .. image:: 
https://github.com/inducer/pytools/workflows/CI/badge.svg?branch=master&event=push
+            :target: https://gitlab.tiker.net/inducer/pytools/commits/main
+        .. image:: 
https://github.com/inducer/pytools/workflows/CI/badge.svg?branch=main&event=push
             :alt: Github Build Status
-            :target: 
https://github.com/inducer/pytools/actions?query=branch%3Amaster+workflow%3ACI+event%3Apush
+            :target: 
https://github.com/inducer/pytools/actions?query=branch%3Amain+workflow%3ACI+event%3Apush
         .. image:: https://badge.fury.io/py/pytools.png
             :alt: Python Package Index Release Page
             :target: https://pypi.org/project/pytools/
@@ -27,7 +27,6 @@
         * A ton of small tool functions such as `len_iterable`, `argmin`,
           tuple generation, permutation generation, ASCII table pretty 
printing,
           GvR's monkeypatch_xxx() hack, the elusive `flatten`, and much more.
-        * Michele Simionato's decorator module
         * A time-series logging module, `pytools.log`.
         * Batch job submission, `pytools.batchjob`.
         * A lexer, `pytools.lex`.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/README.rst 
new/pytools-2021.2.3/README.rst
--- old/pytools-2021.1/README.rst       2020-09-30 01:40:07.000000000 +0200
+++ new/pytools-2021.2.3/README.rst     2021-04-03 00:55:56.000000000 +0200
@@ -1,12 +1,12 @@
 Pytools: Lots of Little Utilities
 =================================
 
-.. image:: https://gitlab.tiker.net/inducer/pytools/badges/master/pipeline.svg
+.. image:: https://gitlab.tiker.net/inducer/pytools/badges/main/pipeline.svg
     :alt: Gitlab Build Status
-    :target: https://gitlab.tiker.net/inducer/pytools/commits/master
-.. image:: 
https://github.com/inducer/pytools/workflows/CI/badge.svg?branch=master&event=push
+    :target: https://gitlab.tiker.net/inducer/pytools/commits/main
+.. image:: 
https://github.com/inducer/pytools/workflows/CI/badge.svg?branch=main&event=push
     :alt: Github Build Status
-    :target: 
https://github.com/inducer/pytools/actions?query=branch%3Amaster+workflow%3ACI+event%3Apush
+    :target: 
https://github.com/inducer/pytools/actions?query=branch%3Amain+workflow%3ACI+event%3Apush
 .. image:: https://badge.fury.io/py/pytools.png
     :alt: Python Package Index Release Page
     :target: https://pypi.org/project/pytools/
@@ -19,7 +19,6 @@
 * A ton of small tool functions such as `len_iterable`, `argmin`,
   tuple generation, permutation generation, ASCII table pretty printing,
   GvR's monkeypatch_xxx() hack, the elusive `flatten`, and much more.
-* Michele Simionato's decorator module
 * A time-series logging module, `pytools.log`.
 * Batch job submission, `pytools.batchjob`.
 * A lexer, `pytools.lex`.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/doc/reference.rst 
new/pytools-2021.2.3/doc/reference.rst
--- old/pytools-2021.1/doc/reference.rst        2019-10-02 00:43:52.000000000 
+0200
+++ new/pytools-2021.2.3/doc/reference.rst      2021-03-23 06:20:01.000000000 
+0100
@@ -1 +1,2 @@
 .. automodule:: pytools
+.. automodule:: pytools.datatable
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/pytools/__init__.py 
new/pytools-2021.2.3/pytools/__init__.py
--- old/pytools-2021.1/pytools/__init__.py      2020-12-08 01:17:06.000000000 
+0100
+++ new/pytools-2021.2.3/pytools/__init__.py    2021-04-03 00:55:56.000000000 
+0200
@@ -25,6 +25,7 @@
 """
 
 
+import re
 from functools import reduce, wraps
 import operator
 import sys
@@ -37,9 +38,6 @@
 from sys import intern
 
 
-decorator_module = __import__("decorator", level=0)
-my_decorator = decorator_module.decorator
-
 # These are deprecated and will go away in 2022.
 all = builtins.all
 any = builtins.any
@@ -73,6 +71,7 @@
 .. autofunction:: memoize_in
 .. autofunction:: keyed_memoize_on_first_arg
 .. autofunction:: keyed_memoize_method
+.. autofunction:: keyed_memoize_in
 
 Argmin/max
 ----------
@@ -163,6 +162,16 @@
 .. autofunction:: natorder
 .. autofunction:: natsorted
 
+Backports of newer Python functionality
+---------------------------------------
+
+.. autofunction:: resolve_name
+
+Hashing
+-------
+
+.. autofunction:: unordered_hash
+
 Type Variables Used
 -------------------
 
@@ -613,43 +622,48 @@
             % ", ".join(list(kwargs.keys())))
 
     if key_func is not None:
-        @my_decorator
-        def _deco(func, *args, **kwargs):
-            # by Michele Simionato
-            # http://www.phyast.pitt.edu/~micheles/python/
-            key = key_func(*args, **kwargs)
-            try:
-                return func._memoize_dic[key]  # pylint: 
disable=protected-access
-            except AttributeError:
-                # _memoize_dic doesn't exist yet.
-                result = func(*args, **kwargs)
-                func._memoize_dic = {key: result}  # pylint: 
disable=protected-access
-                return result
-            except KeyError:
-                result = func(*args, **kwargs)
-                func._memoize_dic[key] = result  # pylint: 
disable=protected-access
-                return result
+        def _decorator(func):
+            def wrapper(*args, **kwargs):
+                key = key_func(*args, **kwargs)
+                try:
+                    return func._memoize_dic[key]  # noqa: E501 # pylint: 
disable=protected-access
+                except AttributeError:
+                    # _memoize_dic doesn't exist yet.
+                    result = func(*args, **kwargs)
+                    func._memoize_dic = {key: result}  # noqa: E501 # pylint: 
disable=protected-access
+                    return result
+                except KeyError:
+                    result = func(*args, **kwargs)
+                    func._memoize_dic[key] = result  # noqa: E501 # pylint: 
disable=protected-access
+                    return result
+
+            from functools import update_wrapper
+            update_wrapper(wrapper, func)
+            return wrapper
+
     else:
-        @my_decorator
-        def _deco(func, *args):
-            # by Michele Simionato
-            # http://www.phyast.pitt.edu/~micheles/python/
-            try:
-                return func._memoize_dic[args]  # pylint: 
disable=protected-access
-            except AttributeError:
-                # _memoize_dic doesn't exist yet.
-                result = func(*args)
-                func._memoize_dic = {args: result}  # 
pylint:disable=protected-access
-                return result
-            except KeyError:
-                result = func(*args)
-                func._memoize_dic[args] = result  # pylint: 
disable=protected-access
-                return result
+        def _decorator(func):
+            def wrapper(*args):
+                try:
+                    return func._memoize_dic[args]  # noqa: E501 # pylint: 
disable=protected-access
+                except AttributeError:
+                    # _memoize_dic doesn't exist yet.
+                    result = func(*args)
+                    func._memoize_dic = {args: result}  # noqa: E501 # 
pylint:disable=protected-access
+                    return result
+                except KeyError:
+                    result = func(*args)
+                    func._memoize_dic[args] = result  # noqa: E501 # pylint: 
disable=protected-access
+                    return result
+
+            from functools import update_wrapper
+            update_wrapper(wrapper, func)
+            return wrapper
 
     if not args:
-        return _deco
+        return _decorator  # type: ignore
     if callable(args[0]) and len(args) == 1:
-        return _deco(args[0])
+        return _decorator(args[0])
     raise TypeError(
         "memoize received unexpected position arguments: %s" % args)
 
@@ -669,8 +683,9 @@
     """
 
     if cache_dict_name is None:
-        cache_dict_name = intern("_memoize_dic_"
-                + function.__module__ + function.__name__)
+        cache_dict_name = intern(
+                f"_memoize_dic_{function.__module__}{function.__name__}"
+                )
 
     def wrapper(obj, *args, **kwargs):
         if kwargs:
@@ -681,16 +696,20 @@
         try:
             return getattr(obj, cache_dict_name)[key]
         except AttributeError:
-            result = function(obj, *args, **kwargs)
-            setattr(obj, cache_dict_name, {key: result})
-            return result
+            attribute_error = True
         except KeyError:
-            result = function(obj, *args, **kwargs)
+            attribute_error = False
+
+        result = function(obj, *args, **kwargs)
+        if attribute_error:
+            object.__setattr__(obj, cache_dict_name, {key: result})
+            return result
+        else:
             getattr(obj, cache_dict_name)[key] = result
             return result
 
     def clear_cache(obj):
-        delattr(obj, cache_dict_name)
+        object.__delattr__(obj, cache_dict_name)
 
     from functools import update_wrapper
     new_wrapper = update_wrapper(wrapper, function)
@@ -701,9 +720,15 @@
 
 def memoize_method(method: F) -> F:
     """Supports cache deletion via ``method_name.clear_cache(self)``.
+
+    .. versionchanged:: 2021.2
+
+        Can memoize methods on classes that do not allow setting attributes
+        (e.g. by overwritting ``__setattr__``), e.g. frozen :mod:`dataclasses`.
     """
 
-    return memoize_on_first_arg(method, 
intern("_memoize_dic_"+method.__name__))
+    return memoize_on_first_arg(method,
+            intern(f"_memoize_dic_{method.__name__}"))
 
 
 class keyed_memoize_on_first_arg:  # noqa: N801
@@ -714,6 +739,8 @@
 
     :arg key: A function receiving the same arguments as the decorated function
         which computes and returns the cache key.
+    :arg cache_dict_name: The name of the `dict` attribute in the instance
+        used to hold the cache.
 
     .. versionadded :: 2020.3
     """
@@ -723,8 +750,7 @@
         self.cache_dict_name = cache_dict_name
 
     def _default_cache_dict_name(self, function):
-        return intern("_memoize_dic_"
-                + function.__module__ + function.__name__)
+        return intern(f"_memoize_dic_{function.__module__}{function.__name__}")
 
     def __call__(self, function):
         cache_dict_name = self.cache_dict_name
@@ -740,7 +766,7 @@
                 return getattr(obj, cache_dict_name)[cache_key]
             except AttributeError:
                 result = function(obj, *args, **kwargs)
-                setattr(obj, cache_dict_name, {cache_key: result})
+                object.__setattr__(obj, cache_dict_name, {cache_key: result})
                 return result
             except KeyError:
                 result = function(obj, *args, **kwargs)
@@ -748,7 +774,7 @@
                 return result
 
         def clear_cache(obj):
-            delattr(obj, cache_dict_name)
+            object.__delattr__(obj, cache_dict_name)
 
         from functools import update_wrapper
         new_wrapper = update_wrapper(wrapper, function)
@@ -758,15 +784,23 @@
 
 
 class keyed_memoize_method(keyed_memoize_on_first_arg):  # noqa: N801
-    """Supports cache deletion via ``method_name.clear_cache(self)``.
+    """Like :class:`memoize_method`, but additionally uses a function *key* to
+    compute the key under which the function result is stored.
+
+    Supports cache deletion via ``method_name.clear_cache(self)``.
 
     :arg key: A function receiving the same arguments as the decorated function
         which computes and returns the cache key.
 
     .. versionadded :: 2020.3
+
+    .. versionchanged:: 2021.2
+
+        Can memoize methods on classes that do not allow setting attributes
+        (e.g. by overwritting ``__setattr__``), e.g. frozen :mod:`dataclasses`.
     """
     def _default_cache_dict_name(self, function):
-        return intern("_memoize_dic_" + function.__name__)
+        return intern(f"_memoize_dic_{function.__name__}")
 
 
 def memoize_method_with_uncached(uncached_args=None, uncached_kwargs=None):
@@ -791,7 +825,7 @@
     uncached_kwargs = list(uncached_kwargs)
 
     def parametrized_decorator(method):
-        cache_dict_name = intern("_memoize_dic_"+method.__name__)
+        cache_dict_name = intern(f"_memoize_dic_{method.__name__}")
 
         def wrapper(self, *args, **kwargs):
             cache_args = list(args)
@@ -817,7 +851,7 @@
                 return getattr(self, cache_dict_name)[key]
             except AttributeError:
                 result = method(self, *args, **kwargs)
-                setattr(self, cache_dict_name, {key: result})
+                object.__setattr__(self, cache_dict_name, {key: result})
                 return result
             except KeyError:
                 result = method(self, *args, **kwargs)
@@ -825,7 +859,7 @@
                 return result
 
         def clear_cache(self):
-            delattr(self, cache_dict_name)
+            object.__delattr__(self, cache_dict_name)
 
         if sys.version_info >= (2, 5):
             from functools import update_wrapper
@@ -837,55 +871,28 @@
     return parametrized_decorator
 
 
-def memoize_method_nested(inner):
-    """Adds a cache to a function nested inside a method. The cache is attached
-    to *memoize_cache_context* (if it exists) or *self* in the outer (method)
-    namespace.
-
-    Requires Python 2.5 or newer.
-    """
-
-    from warnings import warn
-    warn("memoize_method_nested is deprecated and will go away in 2021. "
-            "Use @memoize_in(self, 'identifier') instead", DeprecationWarning,
-            stacklevel=2)
-
-    cache_dict_name = intern("_memoize_inner_dic_%s_%s_%d"
-            % (inner.__name__, inner.__code__.co_filename,
-                inner.__code__.co_firstlineno))
-
-    from inspect import currentframe
-    outer_frame = currentframe().f_back
-    cache_context = outer_frame.f_locals.get("memoize_cache_context")
-    if cache_context is None:
-        cache_context = outer_frame.f_locals.get("self")
-
-    try:
-        cache_dict = getattr(cache_context, cache_dict_name)
-    except AttributeError:
-        cache_dict = {}
-        setattr(cache_context, cache_dict_name, cache_dict)
-
-    @wraps(inner)
-    def new_inner(*args):
-        try:
-            return cache_dict[args]
-        except KeyError:
-            result = inner(*args)
-            cache_dict[args] = result
-            return result
-
-    return new_inner
+class memoize_in:  # noqa
+    """Adds a cache to the function it decorates. The cache is attached
+    to *container* and must be uniquely specified by *identifier* (i.e.
+    all functions using the same *container* and *identifier* will be using
+    the same cache). The decorated function may only receive positional
+    arguments.
 
+    .. note::
 
-class memoize_in:  # noqa
-    """Adds a cache to a function nested inside a method. The cache is attached
-    to *container*.
+        This function works well on nested functions, which
+        do not have stable global identifiers.
 
     .. versionchanged :: 2020.3
 
         *identifier* no longer needs to be a :class:`str`,
         but it needs to be hashable.
+
+    .. versionchanged:: 2021.2.1
+
+        Can now use instances of classes as *container* that do not allow
+        setting attributes (e.g. by overwritting ``__setattr__``),
+        e.g. frozen :mod:`dataclasses`.
     """
 
     def __init__(self, container, identifier):
@@ -893,7 +900,8 @@
             memoize_in_dict = container._pytools_memoize_in_dict
         except AttributeError:
             memoize_in_dict = {}
-            container._pytools_memoize_in_dict = memoize_in_dict
+            object.__setattr__(container, "_pytools_memoize_in_dict",
+                    memoize_in_dict)
 
         self.cache_dict = memoize_in_dict.setdefault(identifier, {})
 
@@ -909,6 +917,41 @@
 
         return new_inner
 
+
+class keyed_memoize_in:  # noqa
+    """Like :class:`memoize_in`, but additionally uses a function *key* to
+    compute the key under which the function result is memoized.
+
+    :arg key: A function receiving the same arguments as the decorated function
+        which computes and returns the cache key.
+
+    .. versionadded :: 2021.2.1
+    """
+
+    def __init__(self, container, identifier, key):
+        try:
+            memoize_in_dict = container._pytools_keyed_memoize_in_dict
+        except AttributeError:
+            memoize_in_dict = {}
+            object.__setattr__(container, "_pytools_keyed_memoize_in_dict",
+                    memoize_in_dict)
+
+        self.cache_dict = memoize_in_dict.setdefault(identifier, {})
+        self.key = key
+
+    def __call__(self, inner):
+        @wraps(inner)
+        def new_inner(*args):
+            key = self.key(*args)
+            try:
+                return self.cache_dict[key]
+            except KeyError:
+                result = inner(*args)
+                self.cache_dict[key] = result
+                return result
+
+        return new_inner
+
 # }}}
 
 
@@ -2397,7 +2440,11 @@
             # Can happen, e.g., if pudb is controlling the console.
             use_late_start_logging = False
         else:
-            use_late_start_logging = sys.stdin.isatty()
+            if hasattr(sys.stdin, "closed") and not sys.stdin.closed:
+                # can query stdin.isatty() only if stdin's open
+                use_late_start_logging = sys.stdin.isatty()
+            else:
+                use_late_start_logging = False
 
         import os
         if os.environ.get("PYTOOLS_LOG_NO_THREADS", ""):
@@ -2542,6 +2589,107 @@
 
 # }}}
 
+
+# {{{ resolve_name
+
+# 
https://github.com/python/cpython/commit/1ed61617a4a6632905ad6a0b440cd2cafb8b6414
+
+_DOTTED_WORDS = r"[a-z_]\w*(\.[a-z_]\w*)*"
+_NAME_PATTERN = re.compile(f"^({_DOTTED_WORDS})(:({_DOTTED_WORDS})?)?$", re.I)
+del _DOTTED_WORDS
+
+
+def resolve_name(name):
+    """A backport of :func:`pkgutil.resolve_name` (added in Python 3.9).
+
+    .. versionadded:: 2021.1.2
+    """
+    # Delete the tail of the function and deprecate this once we require 
Python 3.9.
+    if sys.version_info >= (3, 9):
+        # use the official version
+        import pkgutil
+        return pkgutil.resolve_name(name)  # pylint: disable=no-member
+
+    import importlib
+
+    m = _NAME_PATTERN.match(name)
+    if not m:
+        raise ValueError(f"invalid format: {name!r}")
+    groups = m.groups()
+    if groups[2]:
+        # there is a colon - a one-step import is all that's needed
+        mod = importlib.import_module(groups[0])
+        parts = groups[3].split(".") if groups[3] else []
+    else:
+        # no colon - have to iterate to find the package boundary
+        parts = name.split(".")
+        modname = parts.pop(0)
+        # first part *must* be a module/package.
+        mod = importlib.import_module(modname)
+        while parts:
+            p = parts[0]
+            s = f"{modname}.{p}"
+            try:
+                mod = importlib.import_module(s)
+                parts.pop(0)
+                modname = s
+            except ImportError:
+                break
+    # if we reach this point, mod is the module, already imported, and
+    # parts is the list of parts in the object hierarchy to be traversed, or
+    # an empty list if just the module is wanted.
+    result = mod
+    for p in parts:
+        result = getattr(result, p)
+    return result
+
+# }}}
+
+
+# {{{ unordered_hash
+
+def unordered_hash(hash_instance, iterable, hash_constructor=None):
+    """Using a hash algorithm given by the parameter-less constructor
+    *hash_constructor*, return a hash object whose internal state
+    depends on the entries of *iterable*, but not their order. If *hash*
+    is the instance returned by evaluating ``hash_constructor()``, then
+    the each entry *i* of the iterable must permit ``hash.upate(i)`` to
+    succeed. An example of *hash_constructor* is ``hashlib.sha256``
+    from :mod:`hashlib`.  ``hash.digest_size`` must also be defined.
+    If *hash_constructor* is not provided, ``hash_instance.name`` is
+    used to deduce it.
+
+    :returns: the updated *hash_instance*.
+
+    .. warning::
+
+        The construction used in this function is likely not cryptographically
+        secure. Do not use this function in a security-relevant context.
+
+    .. versionadded:: 2021.2
+    """
+
+    if hash_constructor is None:
+        from functools import partial
+        import hashlib
+        hash_constructor = partial(hashlib.new, hash_instance.name)
+
+    h_int = 0
+    for i in iterable:
+        h_i = hash_constructor()
+        h_i.update(i)
+        # Using sys.byteorder (for efficiency) here technically makes the
+        # hash system-dependent (which it should not be), however the
+        # effect of this is undone by the to_bytes conversion below, while
+        # left invariant by the intervening XOR operations (which do not
+        # mix adjacent bits).
+        h_int = h_int ^ int.from_bytes(h_i.digest(), sys.byteorder)
+
+    hash_instance.update(h_int.to_bytes(hash_instance.digest_size, 
sys.byteorder))
+    return hash_instance
+
+# }}}
+
 
 def _test():
     import doctest
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/pytools/convergence.py 
new/pytools-2021.2.3/pytools/convergence.py
--- old/pytools-2021.1/pytools/convergence.py   2020-12-07 21:54:53.000000000 
+0100
+++ new/pytools-2021.2.3/pytools/convergence.py 2021-03-23 06:20:01.000000000 
+0100
@@ -30,6 +30,11 @@
         abscissae = np.array([a for a, e in self.history])
         errors = np.array([e for a, e in self.history])
 
+        # NOTE: in case any of the errors are exactly 0.0, which
+        # can give NaNs in `estimate_order_of_convergence`
+        emax = np.amax(errors)
+        errors += (1 if emax == 0 else emax) * np.finfo(errors.dtype).eps
+
         size = len(abscissae)
         if gliding_mean is None:
             gliding_mean = size
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/pytools/datatable.py 
new/pytools-2021.2.3/pytools/datatable.py
--- old/pytools-2021.1/pytools/datatable.py     2020-12-07 21:54:53.000000000 
+0100
+++ new/pytools-2021.2.3/pytools/datatable.py   2021-03-23 06:20:01.000000000 
+0100
@@ -1,19 +1,33 @@
 from pytools import Record
 
 
+__doc__ = """
+An in-memory relational database table
+======================================
+
+.. autoclass:: DataTable
+"""
+
+
 class Row(Record):
     pass
 
 
 class DataTable:
-    """An in-memory relational database table."""
+    """An in-memory relational database table.
+
+    .. automethod:: __init__
+    .. automethod:: copy
+    .. automethod:: deep_copy
+    .. automethod:: join
+    """
 
     def __init__(self, column_names, column_data=None):
         """Construct a new table, with the given C{column_names}.
 
-        @arg column_names: An indexable of column name strings.
-        @arg column_data: None or a list of tuples of the same length as
-          C{column_names} indicating an initial set of data.
+        :arg column_names: An indexable of column name strings.
+        :arg column_data: None or a list of tuples of the same length as
+             *column_names* indicating an initial set of data.
         """
         if column_data is None:
             self.data = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/pytools/decorator.py 
new/pytools-2021.2.3/pytools/decorator.py
--- old/pytools-2021.1/pytools/decorator.py     2020-12-07 21:54:53.000000000 
+0100
+++ new/pytools-2021.2.3/pytools/decorator.py   1970-01-01 01:00:00.000000000 
+0100
@@ -1,159 +0,0 @@
-# Python decorator module
-# by Michele Simionato
-# http://www.phyast.pitt.edu/~micheles/python/
-
-## The basic trick is to generate the source code for the decorated function
-## with the right signature and to evaluate it.
-## Uncomment the statement 'print >> sys.stderr, func_src'  in _decorate
-## to understand what is going on.
-
-__all__ = ["decorator", "update_wrapper", "getinfo"]
-
-import inspect
-
-def getinfo(func):
-    """
-    Returns an info dictionary containing:
-    - name (the name of the function : str)
-    - argnames (the names of the arguments : list)
-    - defaults (the values of the default arguments : tuple)
-    - signature (the signature : str)
-    - doc (the docstring : str)
-    - module (the module name : str)
-    - dict (the function __dict__ : str)
-
-    >>> def f(self, x=1, y=2, *args, **kw): pass
-
-    >>> info = getinfo(f)
-
-    >>> info["name"]
-    'f'
-    >>> info["argnames"]
-    ['self', 'x', 'y', 'args', 'kw']
-
-    >>> info["defaults"]
-    (1, 2)
-
-    >>> info["signature"]
-    'self, x, y, *args, **kw'
-    """
-    assert inspect.ismethod(func) or inspect.isfunction(func)
-    regargs, varargs, varkwargs, defaults = inspect.getargspec(func)
-    argnames = list(regargs)
-    if varargs:
-        argnames.append(varargs)
-    if varkwargs:
-        argnames.append(varkwargs)
-    signature = inspect.formatargspec(regargs, varargs, varkwargs, defaults,
-                                      formatvalue=lambda value: "")[1:-1]
-    return dict(name=func.__name__, argnames=argnames, signature=signature,
-                defaults = func.__defaults__, doc=func.__doc__,
-                module=func.__module__, dict=func.__dict__,
-                globals=func.__globals__, closure=func.__closure__)
-
-def update_wrapper(wrapper, wrapped, create=False):
-    """
-    An improvement over functools.update_wrapper. By default it works the
-    same, but if the 'create' flag is set, generates a copy of the wrapper
-    with the right signature and update the copy, not the original.
-    Moreovoer, 'wrapped' can be a dictionary with keys 'name', 'doc', 'module',
-    'dict', 'defaults'.
-    """
-    if isinstance(wrapped, dict):
-        infodict = wrapped
-    else: # assume wrapped is a function
-        infodict = getinfo(wrapped)
-    assert not '_wrapper_' in infodict["argnames"], \
-           '"_wrapper_" is a reserved argument name!'
-    if create: # create a brand new wrapper with the right signature
-        src = "lambda %(signature)s: _wrapper_(%(signature)s)" % infodict
-        # import sys; print >> sys.stderr, src # for debugging purposes
-        wrapper = eval(src, dict(_wrapper_=wrapper))
-    try:
-        wrapper.__name__ = infodict['name']
-    except: # Python version < 2.4
-        pass
-    wrapper.__doc__ = infodict['doc']
-    wrapper.__module__ = infodict['module']
-    wrapper.__dict__.update(infodict['dict'])
-    wrapper.__defaults__ = infodict['defaults']
-    return wrapper
-
-# the real meat is here
-def _decorator(caller, func):
-    if not (inspect.ismethod(func) or inspect.isfunction(func)):
-        # skip all the fanciness, just do what works
-        return lambda *args, **kwargs: caller(func, *args, **kwargs)
-
-    infodict = getinfo(func)
-    argnames = infodict['argnames']
-    assert not ('_call_' in argnames or '_func_' in argnames), \
-           'You cannot use _call_ or _func_ as argument names!'
-    src = "lambda %(signature)s: _call_(_func_, %(signature)s)" % infodict
-    dec_func = eval(src, dict(_func_=func, _call_=caller))
-    return update_wrapper(dec_func, func)
-
-def decorator(caller, func=None):
-    """
-    General purpose decorator factory: takes a caller function as
-    input and returns a decorator with the same attributes.
-    A caller function is any function like this::
-
-     def caller(func, *args, **kw):
-         # do something
-         return func(*args, **kw)
-
-    Here is an example of usage:
-
-    >>> @decorator
-    ... def chatty(f, *args, **kw):
-    ...     print("Calling %r" % f.__name__)
-    ...     return f(*args, **kw)
-
-    >>> chatty.__name__
-    'chatty'
-
-    >>> @chatty
-    ... def f(): pass
-    ...
-    >>> f()
-    Calling 'f'
-
-    For sake of convenience, the decorator factory can also be called with
-    two arguments. In this casem ``decorator(caller, func)`` is just a
-    shortcut for ``decorator(caller)(func)``.
-    """
-    from warnings import warn
-    warn("pytools.decorator is deprecated and will be removed in pytools 12. "
-            "Use the 'decorator' module directly instead.",
-            DeprecationWarning, stacklevel=2)
-
-    if func is None: # return a decorator function
-        return update_wrapper(lambda f : _decorator(caller, f), caller)
-    else: # return a decorated function
-        return _decorator(caller, func)
-
-if __name__ == "__main__":
-    import doctest; doctest.testmod()
-
-#######################     LEGALESE    ##################################
-
-##   Redistributions of source code must retain the above copyright 
-##   notice, this list of conditions and the following disclaimer.
-##   Redistributions in bytecode form must reproduce the above copyright
-##   notice, this list of conditions and the following disclaimer in
-##   the documentation and/or other materials provided with the
-##   distribution.
-
-##   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-##   "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-##   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-##   A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-##   HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-##   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-##   BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
-##   OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-##   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
-##   TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
-##   USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
-##   DAMAGE.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/pytools/obj_array.py 
new/pytools-2021.2.3/pytools/obj_array.py
--- old/pytools-2021.1/pytools/obj_array.py     2020-12-07 21:54:53.000000000 
+0100
+++ new/pytools-2021.2.3/pytools/obj_array.py   2021-04-05 20:35:59.000000000 
+0200
@@ -21,8 +21,8 @@
 """
 
 import numpy as np
+from functools import partial, update_wrapper
 from warnings import warn
-from pytools import my_decorator as decorator
 
 
 __doc__ = """
@@ -142,7 +142,10 @@
         return f(ary)
 
 
-obj_array_vectorized = decorator(obj_array_vectorize)
+def obj_array_vectorized(f):
+    wrapper = partial(obj_array_vectorize, f)
+    update_wrapper(wrapper, f)
+    return wrapper
 
 
 def rec_obj_array_vectorize(f, ary):
@@ -168,7 +171,10 @@
         return f(ary)
 
 
-rec_obj_array_vectorized = decorator(rec_obj_array_vectorize)
+def rec_obj_array_vectorized(f):
+    wrapper = partial(rec_obj_array_vectorize, f)
+    update_wrapper(wrapper, f)
+    return wrapper
 
 
 def obj_array_vectorize_n_args(f, *args):
@@ -207,7 +213,25 @@
     return result
 
 
-obj_array_vectorized_n_args = decorator(obj_array_vectorize_n_args)
+def obj_array_vectorized_n_args(f):
+    # Unfortunately, this can't use partial(), as the callable returned by it
+    # will not be turned into a bound method upon attribute access.
+    # This may happen here, because the decorator *could* be used
+    # on methods, since it can "look past" the leading `self` argument.
+    # Only exactly function objects receive this treatment.
+    #
+    # Spec link:
+    # 
https://docs.python.org/3/reference/datamodel.html#the-standard-type-hierarchy
+    # (under "Instance Methods", quote as of Py3.9.4)
+    # > Also notice that this transformation only happens for user-defined 
functions;
+    # > other callable objects (and all non-callable objects) are retrieved
+    # > without transformation.
+
+    def wrapper(*args):
+        return obj_array_vectorize_n_args(f, *args)
+
+    update_wrapper(wrapper, f)
+    return wrapper
 
 
 # {{{ workarounds for https://github.com/numpy/numpy/issues/1740
@@ -365,7 +389,10 @@
         return f(field)
 
 
-as_oarray_func = decorator(with_object_array_or_scalar)
+def as_oarray_func(f):
+    wrapper = partial(with_object_array_or_scalar, f)
+    update_wrapper(wrapper, f)
+    return wrapper
 
 
 def with_object_array_or_scalar_n_args(f, *args):
@@ -398,7 +425,10 @@
         return f(*args)
 
 
-as_oarray_func_n_args = decorator(with_object_array_or_scalar_n_args)
+def as_oarray_func_n_args(f):
+    wrapper = partial(with_object_array_or_scalar_n_args, f)
+    update_wrapper(wrapper, f)
+    return wrapper
 
 
 def oarray_real(ary):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/pytools/persistent_dict.py 
new/pytools-2021.2.3/pytools/persistent_dict.py
--- old/pytools-2021.1/pytools/persistent_dict.py       2020-12-22 
22:32:38.000000000 +0100
+++ new/pytools-2021.2.3/pytools/persistent_dict.py     2021-03-23 
06:20:01.000000000 +0100
@@ -173,7 +173,37 @@
 # {{{ key generation
 
 class KeyBuilder:
+    """A (stateless) object that computes hashes of objects fed to it. 
Subclassing
+    this class permits customizing the computation of hash keys.
+
+    .. automethod:: __call__
+    .. automethod:: rec
+    .. staticmethod:: new_hash()
+
+        Return a new hash instance following the protocol of the ones
+        from :mod:`hashlib`. This will permit switching to different
+        hash algorithms in the future. Subclasses are expected to use
+        this to create new hashes. Not doing so is deprecated and
+        may stop working as early as 2022.
+
+        .. versionadded:: 2021.2
+    """
+
+    # this exists so that we can (conceivably) switch algorithms at some point
+    # down the road
+    new_hash = hashlib.sha256
+
     def rec(self, key_hash, key):
+        """
+        :arg key_hash: the hash object to be updated with the hash of *key*.
+        :arg key: the (immutable) Python object to be hashed.
+        :returns: the updated *key_hash*
+
+        .. versionchanged:: 2021.2
+
+            Now returns the updated *key_hash*.
+        """
+
         digest = None
 
         try:
@@ -187,7 +217,7 @@
             except AttributeError:
                 pass
             else:
-                inner_key_hash = hashlib.sha256()
+                inner_key_hash = self.new_hash()
                 method(inner_key_hash, self)
                 digest = inner_key_hash.digest()
 
@@ -205,7 +235,7 @@
                         method = self.update_for_specific_dtype
 
             if method is not None:
-                inner_key_hash = hashlib.sha256()
+                inner_key_hash = self.new_hash()
                 method(inner_key_hash, key)
                 digest = inner_key_hash.digest()
 
@@ -222,9 +252,10 @@
                 pass
 
         key_hash.update(digest)
+        return key_hash
 
     def __call__(self, key):
-        key_hash = hashlib.sha256()
+        key_hash = self.new_hash()
         self.rec(key_hash, key)
         return key_hash.hexdigest()
 
@@ -232,14 +263,21 @@
 
     @staticmethod
     def update_for_int(key_hash, key):
-        key_hash.update(str(key).encode("utf8"))
+        sz = 8
+        while True:
+            try:
+                key_hash.update(key.to_bytes(sz, byteorder="little", 
signed=True))
+                return
+            except OverflowError:
+                sz *= 2
 
-    update_for_long = update_for_int
-    update_for_bool = update_for_int
+    @staticmethod
+    def update_for_bool(key_hash, key):
+        key_hash.update(str(key).encode("utf8"))
 
     @staticmethod
     def update_for_float(key_hash, key):
-        key_hash.update(repr(key).encode("utf8"))
+        key_hash.update(key.hex().encode("utf8"))
 
     @staticmethod
     def update_for_str(key_hash, key):
@@ -254,8 +292,11 @@
             self.rec(key_hash, obj_i)
 
     def update_for_frozenset(self, key_hash, key):
-        for set_key in sorted(key):
-            self.rec(key_hash, set_key)
+        from pytools import unordered_hash
+
+        unordered_hash(
+            key_hash,
+            (self.rec(self.new_hash(), key_i).digest() for key_i in key))
 
     @staticmethod
     def update_for_NoneType(key_hash, key):  # noqa
@@ -426,7 +467,7 @@
             import appdirs
             container_dir = join(
                     appdirs.user_cache_dir("pytools", "pytools"),
-                    "pdict-v3-{}-py{}".format(
+                    "pdict-v4-{}-py{}".format(
                         identifier,
                         ".".join(str(i) for i in sys.version_info)))
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/pytools/tag.py 
new/pytools-2021.2.3/pytools/tag.py
--- old/pytools-2021.1/pytools/tag.py   2021-01-09 01:08:43.000000000 +0100
+++ new/pytools-2021.2.3/pytools/tag.py 2021-04-02 23:51:53.000000000 +0200
@@ -122,6 +122,17 @@
     def tag_name(self) -> DottedName:
         return DottedName.from_class(type(self))
 
+    def update_persistent_hash(self, key_hash, key_builder):
+        key_builder.rec(key_hash, self.__class__.__qualname__)
+
+        from dataclasses import fields
+        # Fields are ordered consistently, so ordered hashing is OK.
+        #
+        # No need to dispatch to superclass: fields() automatically gives us
+        # fields from the entire class hierarchy.
+        for f in fields(self):
+            key_builder.rec(key_hash, getattr(self, f.name))
+
 # }}}
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/pytools/version.py 
new/pytools-2021.2.3/pytools/version.py
--- old/pytools-2021.1/pytools/version.py       2021-01-09 01:08:43.000000000 
+0100
+++ new/pytools-2021.2.3/pytools/version.py     2021-04-05 20:36:13.000000000 
+0200
@@ -1,3 +1,3 @@
-VERSION = (2021, 1)
+VERSION = (2021, 2, 3)
 VERSION_STATUS = ""
 VERSION_TEXT = ".".join(str(x) for x in VERSION) + VERSION_STATUS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/pytools.egg-info/PKG-INFO 
new/pytools-2021.2.3/pytools.egg-info/PKG-INFO
--- old/pytools-2021.1/pytools.egg-info/PKG-INFO        2021-01-09 
20:56:50.000000000 +0100
+++ new/pytools-2021.2.3/pytools.egg-info/PKG-INFO      2021-04-05 
20:37:03.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: pytools
-Version: 2021.1
+Version: 2021.2.3
 Summary: A collection of tools for Python
 Home-page: http://pypi.python.org/pypi/pytools
 Author: Andreas Kloeckner
@@ -9,12 +9,12 @@
 Description: Pytools: Lots of Little Utilities
         =================================
         
-        .. image:: 
https://gitlab.tiker.net/inducer/pytools/badges/master/pipeline.svg
+        .. image:: 
https://gitlab.tiker.net/inducer/pytools/badges/main/pipeline.svg
             :alt: Gitlab Build Status
-            :target: https://gitlab.tiker.net/inducer/pytools/commits/master
-        .. image:: 
https://github.com/inducer/pytools/workflows/CI/badge.svg?branch=master&event=push
+            :target: https://gitlab.tiker.net/inducer/pytools/commits/main
+        .. image:: 
https://github.com/inducer/pytools/workflows/CI/badge.svg?branch=main&event=push
             :alt: Github Build Status
-            :target: 
https://github.com/inducer/pytools/actions?query=branch%3Amaster+workflow%3ACI+event%3Apush
+            :target: 
https://github.com/inducer/pytools/actions?query=branch%3Amain+workflow%3ACI+event%3Apush
         .. image:: https://badge.fury.io/py/pytools.png
             :alt: Python Package Index Release Page
             :target: https://pypi.org/project/pytools/
@@ -27,7 +27,6 @@
         * A ton of small tool functions such as `len_iterable`, `argmin`,
           tuple generation, permutation generation, ASCII table pretty 
printing,
           GvR's monkeypatch_xxx() hack, the elusive `flatten`, and much more.
-        * Michele Simionato's decorator module
         * A time-series logging module, `pytools.log`.
         * Batch job submission, `pytools.batchjob`.
         * A lexer, `pytools.lex`.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/pytools.egg-info/SOURCES.txt 
new/pytools-2021.2.3/pytools.egg-info/SOURCES.txt
--- old/pytools-2021.1/pytools.egg-info/SOURCES.txt     2021-01-09 
20:56:50.000000000 +0100
+++ new/pytools-2021.2.3/pytools.egg-info/SOURCES.txt   2021-04-05 
20:37:03.000000000 +0200
@@ -19,7 +19,6 @@
 pytools/convergence.py
 pytools/datatable.py
 pytools/debug.py
-pytools/decorator.py
 pytools/graph.py
 pytools/lex.py
 pytools/log.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/pytools.egg-info/requires.txt 
new/pytools-2021.2.3/pytools.egg-info/requires.txt
--- old/pytools-2021.1/pytools.egg-info/requires.txt    2021-01-09 
20:56:50.000000000 +0100
+++ new/pytools-2021.2.3/pytools.egg-info/requires.txt  2021-04-05 
20:37:03.000000000 +0200
@@ -1,5 +1,4 @@
 appdirs>=1.4.0
-decorator>=3.2.0
 numpy>=1.6.0
 
 [:python_version <= "3.6"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/setup.cfg 
new/pytools-2021.2.3/setup.cfg
--- old/pytools-2021.1/setup.cfg        2021-01-09 20:56:50.842988300 +0100
+++ new/pytools-2021.2.3/setup.cfg      2021-04-05 20:37:03.446051100 +0200
@@ -1,7 +1,6 @@
 [flake8]
 ignore = E126,E127,E128,E123,E226,E241,E242,E265,E402,W503,E731
 max-line-length = 85
-exclude = pytools/arithmetic_container.py,pytools/decorator.py
 inline-quotes = "
 docstring-quotes = "
 multiline-quotes = """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/setup.py new/pytools-2021.2.3/setup.py
--- old/pytools-2021.1/setup.py 2020-12-07 21:54:53.000000000 +0100
+++ new/pytools-2021.2.3/setup.py       2021-04-03 00:55:56.000000000 +0200
@@ -38,7 +38,6 @@
       python_requires="~=3.6",
 
       install_requires=[
-          "decorator>=3.2.0",
           "appdirs>=1.4.0",
           "numpy>=1.6.0",
           "dataclasses>=0.7;python_version<='3.6'"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/test/test_persistent_dict.py 
new/pytools-2021.2.3/test/test_persistent_dict.py
--- old/pytools-2021.1/test/test_persistent_dict.py     2020-12-07 
21:54:53.000000000 +0100
+++ new/pytools-2021.2.3/test/test_persistent_dict.py   2021-03-23 
06:20:01.000000000 +0100
@@ -4,6 +4,7 @@
 
 import pytest
 
+from pytools.tag import Tag, tag_dataclass
 from pytools.persistent_dict import (CollisionWarning, NoSuchEntryError,
         PersistentDict, ReadOnlyEntryError, WriteOncePersistentDict)
 
@@ -39,6 +40,11 @@
 # }}}
 
 
+@tag_dataclass
+class SomeTag(Tag):
+    value: str
+
+
 def test_persistent_dict_storage_and_lookup():
     try:
         tmpdir = tempfile.mkdtemp()
@@ -51,7 +57,10 @@
                     chr(65+randrange(26))
                     for i in range(n))
 
-        keys = [(randrange(2000), rand_str(), None) for i in range(20)]
+        keys = [
+                (randrange(2000)-1000, rand_str(), None, SomeTag(rand_str()),
+                    frozenset({"abc", 123}))
+                for i in range(20)]
         values = [randrange(2000) for i in range(20)]
 
         d = dict(list(zip(keys, values)))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytools-2021.1/test/test_pytools.py 
new/pytools-2021.2.3/test/test_pytools.py
--- old/pytools-2021.1/test/test_pytools.py     2021-01-09 01:08:43.000000000 
+0100
+++ new/pytools-2021.2.3/test/test_pytools.py   2021-04-05 20:35:59.000000000 
+0200
@@ -1,3 +1,26 @@
+__copyright__ = "Copyright (C) 2009-2021 Andreas Kloeckner"
+
+__license__ = """
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+
 import sys
 import pytest
 
@@ -49,8 +72,8 @@
     sc.f.clear_cache(sc)  # pylint: disable=no-member
 
 
-def test_memoize_method_nested():
-    from pytools import memoize_method_nested
+def test_memoize_in():
+    from pytools import memoize_in
 
     class SomeClass:
         def __init__(self):
@@ -58,7 +81,7 @@
 
         def f(self):
 
-            @memoize_method_nested
+            @memoize_in(self, (SomeClass.f,))
             def inner(x):
                 self.run_count += 1
                 return 2*x
@@ -97,6 +120,20 @@
     from pytools import memoize
     count = [0]
 
+    @memoize
+    def f(i, j):
+        count[0] += 1
+        return i + j
+
+    assert f(1, 2) == 3
+    assert f(1, 2) == 3
+    assert count[0] == 1
+
+
+def test_memoize_with_kwargs():
+    from pytools import memoize
+    count = [0]
+
     @memoize(use_kwargs=True)
     def f(i, j=1):
         count[0] += 1
@@ -131,6 +168,48 @@
     assert count[0] == 2
 
 
+def test_memoize_frozen():
+    from dataclasses import dataclass
+    from pytools import memoize_method
+
+    # {{{ check frozen dataclass
+
+    @dataclass(frozen=True)
+    class FrozenDataclass:
+        value: int
+
+        @memoize_method
+        def double_value(self):
+            return 2 * self.value
+
+    c = FrozenDataclass(10)
+    assert c.double_value() == 20
+    c.double_value.clear_cache(c)       # pylint: disable=no-member
+
+    # }}}
+
+    # {{{ check class with no setattr
+
+    class FrozenClass:
+        value: int
+
+        def __init__(self, value):
+            object.__setattr__(self, "value", value)
+
+        def __setattr__(self, key, value):
+            raise AttributeError(f"cannot set attribute {key}")
+
+        @memoize_method
+        def double_value(self):
+            return 2 * self.value
+
+    c = FrozenClass(10)
+    assert c.double_value() == 20
+    c.double_value.clear_cache(c)       # pylint: disable=no-member
+
+    # }}}
+
+
 @pytest.mark.parametrize("dims", [2, 3])
 def test_spatial_btree(dims, do_plot=False):
     pytest.importorskip("numpy")
@@ -283,6 +362,86 @@
 # }}}
 
 
+# {{{ test obj array vectorization and decorators
+
+def test_obj_array_vectorize(c=1):
+    np = pytest.importorskip("numpy")
+    la = pytest.importorskip("numpy.linalg")
+
+    # {{{ functions
+
+    import pytools.obj_array as obj
+
+    def add_one(ary):
+        assert ary.dtype.char != "O"
+        return ary + c
+
+    def two_add_one(x, y):
+        assert x.dtype.char != "O" and y.dtype.char != "O"
+        return x * y + c
+
+    @obj.obj_array_vectorized
+    def vectorized_add_one(ary):
+        assert ary.dtype.char != "O"
+        return ary + c
+
+    @obj.obj_array_vectorized_n_args
+    def vectorized_two_add_one(x, y):
+        assert x.dtype.char != "O" and y.dtype.char != "O"
+        return x * y + c
+
+    class Adder:
+        def __init__(self, c):
+            self.c = c
+
+        def add(self, ary):
+            assert ary.dtype.char != "O"
+            return ary + self.c
+
+        @obj.obj_array_vectorized_n_args
+        def vectorized_add(self, ary):
+            assert ary.dtype.char != "O"
+            return ary + self.c
+
+    adder = Adder(c)
+
+    # }}}
+
+    # {{{ check
+
+    scalar_ary = np.ones(42, dtype=np.float)
+    object_ary = obj.make_obj_array([scalar_ary, scalar_ary, scalar_ary])
+
+    for func, vectorizer, nargs in [
+            (add_one, obj.obj_array_vectorize, 1),
+            (two_add_one, obj.obj_array_vectorize_n_args, 2),
+            (adder.add, obj.obj_array_vectorize, 1),
+            ]:
+        input_ary = [scalar_ary] * nargs
+        result = vectorizer(func, *input_ary)
+        error = la.norm(result - c - 1)
+        print(error)
+
+        input_ary = [object_ary] * nargs
+        result = vectorizer(func, *input_ary)
+        error = 0
+
+    for func, nargs in [
+            (vectorized_add_one, 1),
+            (vectorized_two_add_one, 2),
+            (adder.vectorized_add, 1),
+            ]:
+        input_ary = [scalar_ary] * nargs
+        result = func(*input_ary)
+
+        input_ary = [object_ary] * nargs
+        result = func(*input_ary)
+
+    # }}}
+
+# }}}
+
+
 def test_tag():
     from pytools.tag import Taggable, Tag, UniqueTag, NonUniqueTagError
 
@@ -370,6 +529,28 @@
         t4.without_tags(red_ribbon)
 
 
+def test_unordered_hash():
+    import random
+    import hashlib
+
+    # FIXME: Use randbytes once >=3.9 is OK
+    lst = [bytes([random.randrange(256) for _ in range(20)])
+            for _ in range(200)]
+    lorig = lst[:]
+    random.shuffle(lst)
+
+    from pytools import unordered_hash
+    assert (unordered_hash(hashlib.sha256(), lorig).digest()
+            == unordered_hash(hashlib.sha256(), lst).digest())
+    assert (unordered_hash(hashlib.sha256(), lorig).digest()
+            == unordered_hash(hashlib.sha256(), lorig).digest())
+    assert (unordered_hash(hashlib.sha256(), lorig).digest()
+            != unordered_hash(hashlib.sha256(), lorig[:-1]).digest())
+    lst[0] = b"aksdjfla;sdfjafd"
+    assert (unordered_hash(hashlib.sha256(), lorig).digest()
+            != unordered_hash(hashlib.sha256(), lst).digest())
+
+
 if __name__ == "__main__":
     if len(sys.argv) > 1:
         exec(sys.argv[1])

Reply via email to