Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions reframe/core/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion reframe/core/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
)
Expand Down
6 changes: 6 additions & 0 deletions unittests/test_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()

Expand Down