diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f0ee708e8..4438e6a61 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -107,6 +107,7 @@ jobs: - name: Test env: LD_LIBRARY_PATH: ${{ steps.get-lib-dir.outputs.libdir }} + NUTILS_DEBUG: all run: | mkdir testenv cp -r examples docs tests .coveragerc testenv @@ -168,7 +169,7 @@ jobs: - name: Run unit tests env: _image: ${{ steps.build.outputs.id }} - run: podman run --pull=never --rm -v "$PWD/tests:/app/tests:ro" -v "$PWD/examples:/app/examples:ro" "$_image" -m unittest -bq + run: podman run --pull=never --rm -v "$PWD/tests:/app/tests:ro" -v "$PWD/examples:/app/examples:ro" --env NUTILS_DEBUG=all "$_image" -m unittest -bq - name: Push image to container registry if: ${{ github.event_name == 'push' }} env: diff --git a/nutils/debug_flags.py b/nutils/debug_flags.py new file mode 100644 index 000000000..4ee532031 --- /dev/null +++ b/nutils/debug_flags.py @@ -0,0 +1,31 @@ +# Copyright (c) 2020 Evalf +# +# 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 os, warnings + +_env = dict.fromkeys(filter(None, os.getenv('NUTILS_DEBUG', '').lower().split(':')), True) +_all = _env.pop('all', False) + +sparse = _env.pop('sparse', _all or __debug__) # check sparse chunks in evaluable +lower = _env.pop('lower', _all or __debug__) # check lowered shape, dtype in function +evalf = _env.pop('evalf', _all) # check evaluated arrays in evaluable + +if _env: + warnings.warn('unused debug flags: {}'.format(', '.join(_env))) diff --git a/nutils/evaluable.py b/nutils/evaluable.py index e124194ed..3c263c46b 100644 --- a/nutils/evaluable.py +++ b/nutils/evaluable.py @@ -48,7 +48,7 @@ else: Protocol = object -from . import util, types, numeric, cache, transform, expression, warnings, parallel, sparse +from . import debug_flags, util, types, numeric, cache, transform, expression, warnings, parallel, sparse from ._graph import Node, RegularNode, DuplicatedLeafNode, InvisibleNode, Subgraph import numpy, sys, itertools, functools, operator, inspect, numbers, builtins, re, types as builtin_types, abc, collections.abc, math, treelog as log, weakref, time, contextlib, subprocess _ = numpy.newaxis @@ -733,8 +733,9 @@ def as_axis_property(value): # ARRAYS -if __debug__: +_ArrayMeta = type(Evaluable) +if debug_flags.sparse: def _chunked_assparse_checker(orig): assert isinstance(orig, property) @property @@ -757,6 +758,13 @@ def _assparse(self): return chunks return _assparse + class _ArrayMeta(_ArrayMeta): + def __new__(mcls, name, bases, namespace): + if '_assparse' in namespace: + namespace['_assparse'] = _chunked_assparse_checker(namespace['_assparse']) + return super().__new__(mcls, name, bases, namespace) + +if debug_flags.evalf: class _evalf_checker: def __init__(self, orig): self.evalf_obj = getattr(orig, '__get__', lambda *args: orig) @@ -771,18 +779,12 @@ def evalf_with_check(*args, **kwargs): return res return evalf_with_check - class _ArrayMeta(type(Evaluable)): + class _ArrayMeta(_ArrayMeta): def __new__(mcls, name, bases, namespace): - if '_assparse' in namespace: - namespace['_assparse'] = _chunked_assparse_checker(namespace['_assparse']) if 'evalf' in namespace: namespace['evalf'] = _evalf_checker(namespace['evalf']) return super().__new__(mcls, name, bases, namespace) -else: - - _ArrayMeta = type(Evaluable) - class AsEvaluableArray(Protocol): 'Protocol for conversion into an :class:`Array`.' diff --git a/nutils/function.py b/nutils/function.py index 41f938a33..e0340adb6 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -25,7 +25,7 @@ Protocol = object from typing import Tuple, Union, Type, Callable, Sequence, Any, Optional, Iterator, Dict, Mapping, overload, List, Set -from . import evaluable, numeric, util, expression, types, warnings +from . import evaluable, numeric, util, expression, types, warnings, debug_flags from .transformseq import Transforms import builtins, numpy, re, types as builtin_types, itertools, functools, operator, abc, numbers @@ -46,24 +46,23 @@ def lower(self, *, transform_chains: Tuple[evaluable.TransformChain] = (), coord coordinates : sequence of :class:`nutils.evaluable.Array` objects ''' -if __debug__: +_ArrayMeta = type(Lowerable) + +if debug_flags.lower: def _lower(self, **kwargs): result = self._ArrayMeta__lower(**kwargs) assert isinstance(result, evaluable.Array) offset = kwargs['coordinates'][0].ndim-1 if kwargs.get('coordinates', ()) else 0 assert result.ndim == self.ndim + offset - for n, m in zip(result.shape[offset:], self.shape): - if isinstance(m, int): - assert n == m, 'shape mismatch' + assert all(m == n for m, n in zip(result.shape[offset:], self.shape) if isinstance(n, int)), 'shape mismatch' return result - class _ArrayMeta(type(Lowerable)): + + class _ArrayMeta(_ArrayMeta): def __new__(mcls, name, bases, namespace): if 'lower' in namespace: namespace['_ArrayMeta__lower'] = namespace.pop('lower') namespace['lower'] = _lower return super().__new__(mcls, name, bases, namespace) -else: - _ArrayMeta = type class Array(Lowerable, metaclass=_ArrayMeta): '''Base class for array valued functions.