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
21 changes: 14 additions & 7 deletions reframe/core/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ def __prepare__(metacls, name, bases, **kwargs):
namespace['variable'] = variables.TestVar
namespace['required'] = variables.Undefined

# Utility decorators
def bind(fn, name=None):
'''Directive to bind a free function to a class.

Expand All @@ -186,7 +187,14 @@ def bind(fn, name=None):
namespace[inst.__name__] = inst
return inst

def final(fn):
'''Indicate that a function is final and cannot be overridden.'''

fn._rfm_final = True
return fn

namespace['bind'] = bind
namespace['final'] = final

# Hook-related functionality
def run_before(stage):
Expand All @@ -205,8 +213,6 @@ def run_before(stage):

return hooks.attach_to('pre_' + stage)

namespace['run_before'] = run_before

def run_after(stage):
'''Decorator for attaching a test method to a pipeline stage.

Expand All @@ -228,6 +234,7 @@ def run_after(stage):

return hooks.attach_to('post_' + stage)

namespace['run_before'] = run_before
namespace['run_after'] = run_after
namespace['require_deps'] = hooks.require_deps

Expand Down Expand Up @@ -259,7 +266,8 @@ class was created or even at the instance level (e.g. doing

directives = [
'parameter', 'variable', 'bind', 'run_before', 'run_after',
'require_deps', 'required', 'deferrable', 'sanity_function'
'require_deps', 'required', 'deferrable', 'sanity_function',
'final'
]
for b in directives:
namespace.pop(b, None)
Expand Down Expand Up @@ -326,13 +334,12 @@ def __init__(cls, name, bases, namespace, **kwargs):
if hasattr(v, '_rfm_final')}

# Add the final functions from its parents
cls._final_methods.update(*(b._final_methods for b in bases
if hasattr(b, '_final_methods')))
bases_w_final = [b for b in bases if hasattr(b, '_final_methods')]
cls._final_methods.update(*(b._final_methods for b in bases_w_final))

if getattr(cls, '_rfm_special_test', None):
if getattr(cls, '_rfm_override_final', None):
return

bases_w_final = [b for b in bases if hasattr(b, '_final_methods')]
for v in namespace.values():
for b in bases_w_final:
if callable(v) and v.__name__ in b._final_methods:
Expand Down
9 changes: 7 additions & 2 deletions reframe/core/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@

def final(fn):
fn._rfm_final = True
user_deprecation_warning(
'using the @rfm.final decorator from the rfm module is '
'deprecated; please use the built-in decorator @final instead.',
from_version='3.7.0'
)

@functools.wraps(fn)
def _wrapped(*args, **kwargs):
Expand Down Expand Up @@ -802,7 +807,7 @@ def __getattribute__(self, name):
@classmethod
def __init_subclass__(cls, *, special=False, pin_prefix=False, **kwargs):
super().__init_subclass__(**kwargs)
cls._rfm_special_test = special
cls._rfm_override_final = special

# Insert the prefix to pin the test to if the test lives in a test
# library with resources in it.
Expand Down Expand Up @@ -1934,7 +1939,7 @@ def run(self):
self._copy_to_stagedir(os.path.join(self._prefix,
self.sourcesdir))

super().run.__wrapped__(self)
super().run()


class CompileOnlyRegressionTest(RegressionTest, special=True):
Expand Down
20 changes: 8 additions & 12 deletions unittests/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import reframe as rfm
from reframe.core.exceptions import NameConflictError, ReframeSyntaxError
from reframe.core.warnings import ReframeDeprecationWarning
from reframe.frontend.loader import RegressionCheckLoader


Expand Down Expand Up @@ -137,17 +138,12 @@ def __init__(self):
def setup(self, partition, environ, **job_opts):
super().setup(partition, environ, **job_opts)

@rfm.simple_test
class TestFinal(rfm.RegressionTest):
def __init__(self):
pass

@rfm.final
def my_new_final(self):
pass

with pytest.raises(ReframeSyntaxError):
with pytest.warns(ReframeDeprecationWarning):
@rfm.simple_test
class TestFinalDerived(TestFinal):
def my_new_final(self, a, b):
class TestFinal(rfm.RegressionTest):
def __init__(self):
pass

@rfm.final
def my_new_final(self):
pass
21 changes: 21 additions & 0 deletions unittests/test_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class MyTest(MyMeta):
deferrable(ext)
sanity_function(ext)
v = required
final(ext)

def __init__(self):
assert not hasattr(self, 'parameter')
Expand All @@ -63,6 +64,7 @@ def __init__(self):
assert not hasattr(self, 'deferrable')
assert not hasattr(self, 'sanity_function')
assert not hasattr(self, 'required')
assert not hasattr(self, 'final')

MyTest()

Expand Down Expand Up @@ -201,3 +203,22 @@ def hook_b(self):
assert not Bar.hook_in_stage('hook_b', 'pre_compile')
assert Bar.hook_in_stage('hook_c', 'post_run_wait')
assert Bar.hook_in_stage('hook_a', 'pre_sanity')


def test_final(MyMeta):
class Base(MyMeta):
@final
def foo(self):
pass

with pytest.raises(ReframeSyntaxError):
class Derived(Base):
def foo(self):
'''Override attempt.'''

class AllowFinalOverride(Base):
'''Use flag to bypass the final override check.'''
_rfm_override_final = True

def foo(self):
'''Overriding foo is now allowed.'''