diff --git a/reframe/core/meta.py b/reframe/core/meta.py index a1c5fbc729..7902418e48 100644 --- a/reframe/core/meta.py +++ b/reframe/core/meta.py @@ -136,6 +136,10 @@ def __getitem__(self, key): # raise the exception from the base __getitem__. raise err from None + def reset(self, key): + '''Reset an item to rerun it through the __setitem__ logic.''' + self[key] = self[key] + class WrappedFunction: '''Descriptor to wrap a free function as a bound-method. @@ -212,14 +216,32 @@ def __prepare__(metacls, name, bases, **kwargs): namespace['required'] = variables.Undefined # Utility decorators + namespace['_rfm_ext_bound'] = set() + def bind(fn, name=None): '''Directive to bind a free function to a class. See online docs for more information. + + .. note:: + Functions bound using this directive must be re-inspected after + the class body execution has completed. This directive attaches + the external method into the class namespace and returns the + associated instance of the :class:`WrappedFunction`. However, + this instance may be further modified by other ReFrame builtins + such as :func:`run_before`, :func:`run_after`, :func:`final` and + so on after it was added to the namespace, which would bypass + the logic implemented in the :func:`__setitem__` method from the + :class:`MetaNamespace` class. Hence, we track the items set by + this directive in the ``_rfm_ext_bound`` set, so they can be + later re-inspected. ''' inst = metacls.WrappedFunction(fn, name) namespace[inst.__name__] = inst + + # Track the imported external functions + namespace['_rfm_ext_bound'].add(inst.__name__) return inst def final(fn): @@ -324,6 +346,10 @@ class was created or even at the instance level (e.g. doing for b in directives: namespace.pop(b, None) + # Reset the external functions imported through the bind directive. + for item in namespace.pop('_rfm_ext_bound'): + namespace.reset(item) + return super().__new__(metacls, name, bases, dict(namespace), **kwargs) def __init__(cls, name, bases, namespace, **kwargs): diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 87c9caa459..aa0a0c57c6 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -1717,7 +1717,7 @@ def check_performance(self): if self.perf_variables or self._rfm_perf_fns: if hasattr(self, 'perf_patterns'): raise ReframeSyntaxError( - f"assigning a value to 'perf_pattenrs' conflicts ", + f"assigning a value to 'perf_patterns' conflicts ", f"with using the 'performance_function' decorator ", f"or setting a value to 'perf_variables'" ) diff --git a/unittests/test_meta.py b/unittests/test_meta.py index 7b9f12106c..8781e42b2c 100644 --- a/unittests/test_meta.py +++ b/unittests/test_meta.py @@ -81,6 +81,9 @@ class MyTest(MyMeta): bind(ext_fn) bind(ext_fn, name='ext') + # Catch bug #2146 + final(bind(ext_fn, name='my_final')) + # Bound as different objects assert ext_fn is not ext @@ -100,6 +103,9 @@ def __init__(self): assert self.ext_fn() is self assert self.ext() is self + # Catch bug #2146 + assert 'my_final' in MyTest._rfm_final_methods + # Test __get__ MyTest()