From 3d20133a2fc70e8fc0d9e3fe720535b53a628f59 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Sun, 14 Mar 2021 01:07:34 +0100 Subject: [PATCH 1/8] Add support for class directives --- reframe/core/decorators.py | 2 +- reframe/core/deferrable.py | 1 + reframe/core/directives.py | 94 ++++++++++++++++++++++++++++++++++++ reframe/core/meta.py | 34 ++++++++++++- reframe/core/namespaces.py | 4 +- reframe/core/pipeline.py | 3 +- unittests/test_directives.py | 29 +++++++++++ 7 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 reframe/core/directives.py create mode 100644 unittests/test_directives.py diff --git a/reframe/core/decorators.py b/reframe/core/decorators.py index ccd9b10c2d..7395eda0e3 100644 --- a/reframe/core/decorators.py +++ b/reframe/core/decorators.py @@ -210,7 +210,7 @@ def _fn(*args, **kwargs): def run_before(stage): - '''Decorator for attaching a test method to a pipeline stage. + '''Decorator for attaching a test method to a pipeline stage. The method will run just before the specified pipeline stage and it should not accept any arguments except ``self``. diff --git a/reframe/core/deferrable.py b/reframe/core/deferrable.py index 1014d6bb7b..4fa511a4b3 100644 --- a/reframe/core/deferrable.py +++ b/reframe/core/deferrable.py @@ -10,6 +10,7 @@ def deferrable(func): '''Function decorator for converting a function to a deferred expression.''' + @functools.wraps(func) def _deferred(*args, **kwargs): return _DeferredExpression(func, *args, **kwargs) diff --git a/reframe/core/directives.py b/reframe/core/directives.py new file mode 100644 index 0000000000..4d91faebc2 --- /dev/null +++ b/reframe/core/directives.py @@ -0,0 +1,94 @@ +# Copyright 2016-2021 Swiss National Supercomputing Centre (CSCS/ETH Zurich) +# ReFrame Project Developers. See the top-level LICENSE file for details. +# +# SPDX-License-Identifier: BSD-3-Clause + +# +# Base classes to manage the namespace of a regression test. +# + +'''ReFrame Directives + +A directive makes available a method defined in a class to the execution of +the class body during its creation. + +A directive simply captures the arguments passed to it and all directives are +stored in a registry inside the class. When the final object is created, they +will be applied to that instance by calling the target method on the freshly +created object. As soon as they are applied, they set to point to the actual +object method, since they have served their purpose. This allows us to export +functions such as `depends_on()` at the class level. +''' + +import inspect + + +NAMES = ('depends_on',) + + +class _Directive: + '''A test directive. + + A directive captures the arguments passed to it, so as to call an actual + object function later on during the test's initialization. + + ''' + + def __init__(self, name, *args, **kwargs): + self._name = name + self._args = args + self._kwargs = kwargs + + def __repr__(self): + cls = type(self).__qualname__ + return f'{cls}({self.name!r}, {self.args}, {self.kwargs})' + + @property + def name(self): + return self._name + + @property + def args(self): + return self._args + + @property + def kwargs(self): + return self._kwargs + + def apply(self, obj): + fn = getattr(obj, '_D_' + self.name) + fn(*self.args, **self.kwargs) + + +class DirectiveRegistry: + def __init__(self): + self.__directives = [] + + @property + def directives(self): + return self.__directives + + def add(self, name, *args, **kwargs): + self.__directives.append(_Directive(name, *args, **kwargs)) + + +def apply(cls, obj): + '''Apply all directives of class ``cls`` to the object ``obj``.''' + + for d in cls._rfm_dir_registry.directives: + d.apply(obj) + + +def reset(cls): + '''Reset all directives in ``cls`` to point to the corresponding class + methods.''' + + for d in NAMES: + meth_name = '_D_' + d + + # A directive may be defined in a subclass of `RegressionTest`, but + # this will be called on every class that is being created. So we need + # to update the directive only when the target method exists + if hasattr(cls, meth_name): + meth = getattr(cls, meth_name) + setattr(cls, d, meth) diff --git a/reframe/core/meta.py b/reframe/core/meta.py index 16058cd8a6..5bace4d02b 100644 --- a/reframe/core/meta.py +++ b/reframe/core/meta.py @@ -8,16 +8,20 @@ # -from reframe.core.exceptions import ReframeSyntaxError +import functools + +import reframe.core.directives as directives import reframe.core.namespaces as namespaces import reframe.core.parameters as parameters import reframe.core.variables as variables +from reframe.core.exceptions import ReframeSyntaxError class RegressionTestMeta(type): class MetaNamespace(namespaces.LocalNamespace): '''Custom namespace to control the cls attribute assignment.''' + def __setitem__(self, key, value): if isinstance(value, variables.VarDirective): # Insert the attribute in the variable namespace @@ -79,6 +83,14 @@ def __prepare__(metacls, name, bases, **kwargs): # Directives to add/modify a regression test variable namespace['variable'] = variables.TestVar namespace['required'] = variables.UndefineVar() + + # Insert the directives + dir_registry = directives.DirectiveRegistry() + for fn_name in directives.NAMES: + fn_dir_add = functools.partial(dir_registry.add, fn_name) + namespace[fn_name] = fn_dir_add + + namespace['_rfm_dir_registry'] = dir_registry return metacls.MetaNamespace(namespace) def __new__(metacls, name, bases, namespace, **kwargs): @@ -127,6 +139,23 @@ def __init__(cls, name, bases, namespace, **kwargs): if fn_with_deps: hooks['post_setup'] = fn_with_deps + hooks.get('post_setup', []) + def apply_directives(obj): + directives.apply(cls, obj) + directives.reset(cls) + + if hasattr(cls, '__init__'): + orig_init = cls.__init__ + + def replace_init(obj, *args, **kwargs): + apply_directives(obj) + orig_init(obj, *args, **kwargs) + + cls.__init__ = replace_init + else: + hooks['pre_init'] = [ + lambda obj: apply_directives(obj) + ] + cls._rfm_pipeline_hooks = hooks cls._rfm_disabled_hooks = set() cls._final_methods = {v.__name__ for v in namespace.values() @@ -140,6 +169,9 @@ def __init__(cls, name, bases, namespace, **kwargs): return for v in namespace.values(): + if not hasattr(v, '__name__'): + continue + for b in bases: if not hasattr(b, '_final_methods'): continue diff --git a/reframe/core/namespaces.py b/reframe/core/namespaces.py index 13ce9dc62c..514498582b 100644 --- a/reframe/core/namespaces.py +++ b/reframe/core/namespaces.py @@ -10,6 +10,8 @@ import abc +import reframe.core.directives as directives + class LocalNamespace: '''Local namespace of a regression test. @@ -40,7 +42,7 @@ def __getitem__(self, key): return self._namespace[key] def __setitem__(self, key, value): - if key not in self._namespace: + if key not in self._namespace and key not in directives.NAMES: self._namespace[key] = value else: self._raise_namespace_clash(key) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index f6e4ddc34c..658cbc5ef9 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -1711,7 +1711,7 @@ def exact(src, dst): else: raise ValueError(f"unknown value passed to 'how' argument: {how}") - def depends_on(self, target, how=None, *args, **kwargs): + def _D_depends_on(self, target, how=None, *args, **kwargs): '''Add a dependency to another test. :arg target: The name of the test that this one will depend on. @@ -1771,6 +1771,7 @@ def by_part(src, dst): ``subdeps`` argument is deprecated. ''' + if not isinstance(target, str): raise TypeError("target argument must be of type: `str'") diff --git a/unittests/test_directives.py b/unittests/test_directives.py new file mode 100644 index 0000000000..af629cd869 --- /dev/null +++ b/unittests/test_directives.py @@ -0,0 +1,29 @@ +# Copyright 2016-2021 Swiss National Supercomputing Centre (CSCS/ETH Zurich) +# ReFrame Project Developers. See the top-level LICENSE file for details. +# +# SPDX-License-Identifier: BSD-3-Clause + +import pytest + +import reframe as rfm +import reframe.core.directives as directives + + +def test_directives(monkeypatch): + monkeypatch.setattr(directives, 'NAMES', ('foo',)) + + class _Base(rfm.RegressionTest): + def _D_foo(self, x): + self.x = x + + class _Derived_1(_Base): + foo(1) + + class _Derived_2(_Base): + def __init__(self): + self.foo(1) + + t1 = _Derived_1() + t2 = _Derived_2() + assert t1.x == 1 + assert t2.x == 1 From d5e343b070d48807ea93c648a9c1e66b0cb406d5 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Mon, 15 Mar 2021 14:27:00 +0100 Subject: [PATCH 2/8] Remove unused imports --- reframe/core/directives.py | 3 --- unittests/test_directives.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/reframe/core/directives.py b/reframe/core/directives.py index 4d91faebc2..8f8ef8b027 100644 --- a/reframe/core/directives.py +++ b/reframe/core/directives.py @@ -20,9 +20,6 @@ functions such as `depends_on()` at the class level. ''' -import inspect - - NAMES = ('depends_on',) diff --git a/unittests/test_directives.py b/unittests/test_directives.py index af629cd869..bfa1cf7789 100644 --- a/unittests/test_directives.py +++ b/unittests/test_directives.py @@ -3,8 +3,6 @@ # # SPDX-License-Identifier: BSD-3-Clause -import pytest - import reframe as rfm import reframe.core.directives as directives From 234b3ac9dffc12ece0d9e0c3a8f6d303482157c0 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Tue, 16 Mar 2021 00:07:30 +0100 Subject: [PATCH 3/8] Apply directives in `__rfm_init__` instead of `__init__` This avoids side effects when users override `__init__()` without calling `super()`. Also renamed `_rfm_init()` to `__rfm_init__()` and added a couple more test cases in the unit tests. --- reframe/core/meta.py | 14 +++++--------- reframe/core/pipeline.py | 5 +++-- unittests/test_directives.py | 17 +++++++++++++++-- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/reframe/core/meta.py b/reframe/core/meta.py index 5bace4d02b..22ef7f8b58 100644 --- a/reframe/core/meta.py +++ b/reframe/core/meta.py @@ -143,18 +143,14 @@ def apply_directives(obj): directives.apply(cls, obj) directives.reset(cls) - if hasattr(cls, '__init__'): - orig_init = cls.__init__ + if hasattr(cls, '__rfm_init__'): + orig_rfm_init = cls.__rfm_init__ - def replace_init(obj, *args, **kwargs): + def augmented_init(obj, *args, **kwargs): + orig_rfm_init(obj, *args, **kwargs) apply_directives(obj) - orig_init(obj, *args, **kwargs) - cls.__init__ = replace_init - else: - hooks['pre_init'] = [ - lambda obj: apply_directives(obj) - ] + cls.__rfm_init__ = augmented_init cls._rfm_pipeline_hooks = hooks cls._rfm_disabled_hooks = set() diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 658cbc5ef9..c317ed8911 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -787,7 +787,7 @@ def __new__(cls, *args, _rfm_use_params=False, **kwargs): os.path.dirname(inspect.getfile(cls)) ) - obj._rfm_init(name, prefix) + obj.__rfm_init__(name, prefix) return obj @_run_hooks() @@ -813,7 +813,7 @@ def __init_subclass__(cls, *, special=False, pin_prefix=False, **kwargs): os.path.dirname(inspect.getfile(cls)) ) - def _rfm_init(self, name=None, prefix=None): + def __rfm_init__(self, name=None, prefix=None): if name is not None: self.name = name @@ -872,6 +872,7 @@ def _rfm_init(self, name=None, prefix=None): self._cdt_environ = env.Environment('__rfm_cdt_environ') # Export read-only views to interesting fields + @property def current_environ(self): '''The programming environment that the regression test is currently diff --git a/unittests/test_directives.py b/unittests/test_directives.py index bfa1cf7789..5de4608dd3 100644 --- a/unittests/test_directives.py +++ b/unittests/test_directives.py @@ -19,9 +19,22 @@ class _Derived_1(_Base): class _Derived_2(_Base): def __init__(self): - self.foo(1) + self.foo(2) + + class _Derived_3(_Derived_1): + pass + + class _Derived_4(_Derived_1): + # Verify that inheritance works even if we redefine __init__() + # completely + def __init__(self): + pass t1 = _Derived_1() t2 = _Derived_2() + t3 = _Derived_3() + t4 = _Derived_4() assert t1.x == 1 - assert t2.x == 1 + assert t2.x == 2 + assert t3.x == 1 + assert t4.x == 1 From 5816767005f799fa9c431fd3e3b0f3fe76c988f0 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Wed, 24 Mar 2021 17:07:53 +0100 Subject: [PATCH 4/8] Add skip and skip_if directives and address PR comments --- reframe/core/directives.py | 2 +- reframe/core/namespaces.py | 2 +- reframe/core/pipeline.py | 82 +++++++++++++++++++------------------- 3 files changed, 44 insertions(+), 42 deletions(-) diff --git a/reframe/core/directives.py b/reframe/core/directives.py index 8f8ef8b027..6ee45b8ae8 100644 --- a/reframe/core/directives.py +++ b/reframe/core/directives.py @@ -20,7 +20,7 @@ functions such as `depends_on()` at the class level. ''' -NAMES = ('depends_on',) +NAMES = ('depends_on', 'skip', 'skip_if') class _Directive: diff --git a/reframe/core/namespaces.py b/reframe/core/namespaces.py index 61ae0a5ab6..5fcc30640e 100644 --- a/reframe/core/namespaces.py +++ b/reframe/core/namespaces.py @@ -42,7 +42,7 @@ def __getitem__(self, key): return self._namespace[key] def __setitem__(self, key, value): - if key not in self._namespace and key not in directives.NAMES: + if key not in self._namespace: self._namespace[key] = value else: self._raise_namespace_clash(key) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index ed97a4bec1..5f1ede41c7 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -1735,6 +1735,46 @@ def exact(src, dst): else: raise ValueError(f"unknown value passed to 'how' argument: {how}") + def getdep(self, target, environ=None, part=None): + '''Retrieve the test case of a target dependency. + + This is a low-level method. The :func:`@require_deps + ` decorators should be + preferred. + + :arg target: The name of the target dependency to be retrieved. + :arg environ: The name of the programming environment that will be + used to retrieve the test case of the target test. If ``None``, + :attr:`RegressionTest.current_environ` will be used. + + .. versionadded:: 2.21 + + ''' + if self.current_environ is None: + raise DependencyError( + 'cannot resolve dependencies before the setup phase' + ) + + if environ is None: + environ = self.current_environ.name + + if part is None: + part = self.current_partition.name + + if self._case is None or self._case() is None: + raise DependencyError('no test case is associated with this test') + + for d in self._case().deps: + if (d.check.name == target and + d.environ.name == environ and + d.partition.name == part): + return d.check + + raise DependencyError(f'could not resolve dependency to ({target!r}, ' + f'{part!r}, {environ!r})') + + # Directives + def _D_depends_on(self, target, how=None, *args, **kwargs): '''Add a dependency to another test. @@ -1812,45 +1852,7 @@ def by_part(src, dst): self._userdeps.append((target, how)) - def getdep(self, target, environ=None, part=None): - '''Retrieve the test case of a target dependency. - - This is a low-level method. The :func:`@require_deps - ` decorators should be - preferred. - - :arg target: The name of the target dependency to be retrieved. - :arg environ: The name of the programming environment that will be - used to retrieve the test case of the target test. If ``None``, - :attr:`RegressionTest.current_environ` will be used. - - .. versionadded:: 2.21 - - ''' - if self.current_environ is None: - raise DependencyError( - 'cannot resolve dependencies before the setup phase' - ) - - if environ is None: - environ = self.current_environ.name - - if part is None: - part = self.current_partition.name - - if self._case is None or self._case() is None: - raise DependencyError('no test case is associated with this test') - - for d in self._case().deps: - if (d.check.name == target and - d.environ.name == environ and - d.partition.name == part): - return d.check - - raise DependencyError(f'could not resolve dependency to ({target!r}, ' - f'{part!r}, {environ!r})') - - def skip(self, msg=None): + def _D_skip(self, msg=None): '''Skip test. :arg msg: A message explaining why the test was skipped. @@ -1859,7 +1861,7 @@ def skip(self, msg=None): ''' raise SkipTestError(msg) - def skip_if(self, cond, msg=None): + def _D_skip_if(self, cond, msg=None): '''Skip test if condition is true. :arg cond: The condition to check for skipping the test. From 170fc4fff73e129ae6a122979d77dea7fbf57adb Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Wed, 24 Mar 2021 21:56:20 +0100 Subject: [PATCH 5/8] Document the directives --- docs/regression_test_api.rst | 82 ++++++++++++++++++++++++++++++++++++ reframe/core/pipeline.py | 73 ++------------------------------ 2 files changed, 85 insertions(+), 70 deletions(-) diff --git a/docs/regression_test_api.rst b/docs/regression_test_api.rst index d245079dc6..ee2bd7e34e 100644 --- a/docs/regression_test_api.rst +++ b/docs/regression_test_api.rst @@ -216,6 +216,88 @@ In essence, these builtins exert control over the test creation, and they allow :class:`reframe.core.fields.Field`. +Directives +---------- + +.. versionadded:: 3.5.2 + +Directives are special functions that are called at the class level but will be applied to the newly created test. +Directives can also be invoked as normal test methods once the test has been created. +Using directives and `builtins <#builtins>`__ together, it is possible to completely get rid of the :func:`__init__` method in the tests. +Static test information can be defined in the test class body and any adaptations based on the current system and/or environment can be made inside `pipeline hooks <#pipeline-hooks>`__. + +.. py:function:: RegressionTest.depends_on(target, how=None, *args, **kwargs) + + .. versionadded:: 2.21 (as a test method) + + Add a dependency to another test. + + :arg target: The name of the test that this one will depend on. + :arg how: A callable that defines how the test cases of this test depend on the the test cases of the target test. This callable should accept two arguments: + + - The source test case (i.e., a test case of this test) represented as a two-element tuple containing the names of the partition and the environment of the current test case. + - Test destination test case (i.e., a test case of the target test) represented as a two-element tuple containing the names of the partition and the environment of the current target test case. + + It should return :class:`True` if a dependency between the source and destination test cases exists, :class:`False` otherwise. + + This function will be called multiple times by the framework when the test DAG is constructed, in order to determine the connectivity of the two tests. + + In the following example, test ``T1`` depends on ``T0`` when their partitions match, otherwise their test cases are independent. + + .. code-block:: python + + def by_part(src, dst): + p0, _ = src + p1, _ = dst + return p0 == p1 + + class T1(rfm.RegressionTest): + depends_on('T0', how=by_part) + + The framework offers already a set of predefined relations between the test cases of inter-dependent tests. See the :mod:`reframe.utility.udeps` for more details. + + The default ``how`` function is :func:`~reframe.utility.udeps.by_case`, where test cases on different partitions and environments are independent. + + .. seealso:: + - :doc:`dependencies` + - :ref:`test-case-deps-management` + + .. versionchanged:: 3.3 + Dependencies between test cases from different partitions are now allowed. The ``how`` argument now accepts a callable. + + .. deprecated:: 3.3 + Passing an integer to the ``how`` argument as well as using the ``subdeps`` argument is deprecated. + + .. versionchanged:: 3.5.2 + This function has become a directive. + + +.. py:function:: RegressionTest.skip(msg=None) + + Skip test. + + :arg msg: A message explaining why the test was skipped. + + .. versionadded:: 3.5.1 + + .. versionchanged:: 3.5.2 + This function has become a directive. + + +.. py:function:: RegressionTest.skip_if(cond, msg=None) + + Skip test if condition is true. + + :arg cond: The condition to check for skipping the test. + :arg msg: A message explaining why the test was skipped. + + .. versionadded:: 3.5.1 + + .. versionchanged:: 3.5.2 + This function has become a directive. + + + Environments and Systems ------------------------ diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 5f1ede41c7..c1c45435e6 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -1776,65 +1776,7 @@ def getdep(self, target, environ=None, part=None): # Directives def _D_depends_on(self, target, how=None, *args, **kwargs): - '''Add a dependency to another test. - - :arg target: The name of the test that this one will depend on. - :arg how: A callable that defines how the test cases of this test - depend on the the test cases of the target test. - This callable should accept two arguments: - - - The source test case (i.e., a test case of this test) - represented as a two-element tuple containing the names of the - partition and the environment of the current test case. - - Test destination test case (i.e., a test case of the target - test) represented as a two-element tuple containing the names of - the partition and the environment of the current target test - case. - - It should return :class:`True` if a dependency between the source - and destination test cases exists, :class:`False` otherwise. - - This function will be called multiple times by the framework when - the test DAG is constructed, in order to determine the - connectivity of the two tests. - - In the following example, this test depends on ``T1`` when their - partitions match, otherwise their test cases are independent. - - .. code-block:: python - - def by_part(src, dst): - p0, _ = src - p1, _ = dst - return p0 == p1 - - self.depends_on('T0', how=by_part) - - The framework offers already a set of predefined relations between - the test cases of inter-dependent tests. See the - :mod:`reframe.utility.udeps` for more details. - - The default ``how`` function is - :func:`reframe.utility.udeps.by_case`, where test cases on - different partitions and environments are independent. - - .. seealso:: - - :doc:`dependencies` - - :ref:`test-case-deps-management` - - - - .. versionadded:: 2.21 - - .. versionchanged:: 3.3 - Dependencies between test cases from different partitions are now - allowed. The ``how`` argument now accepts a callable. - - .. deprecated:: 3.3 - Passing an integer to the ``how`` argument as well as using the - ``subdeps`` argument is deprecated. - - ''' + '''Add a dependency to another test.''' if not isinstance(target, str): raise TypeError("target argument must be of type: `str'") @@ -1853,22 +1795,13 @@ def by_part(src, dst): self._userdeps.append((target, how)) def _D_skip(self, msg=None): - '''Skip test. - - :arg msg: A message explaining why the test was skipped. + '''Skip test.''' - .. versionadded:: 3.5.1 - ''' raise SkipTestError(msg) def _D_skip_if(self, cond, msg=None): - '''Skip test if condition is true. + '''Skip test if condition is true.''' - :arg cond: The condition to check for skipping the test. - :arg msg: A message explaining why the test was skipped. - - .. versionadded:: 3.5.1 - ''' if cond: self.skip(msg) From 47fcb19f83ef4e6a15ddd628d682965adcd6be20 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Wed, 24 Mar 2021 23:21:38 +0100 Subject: [PATCH 6/8] Remove unused import --- reframe/core/namespaces.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/reframe/core/namespaces.py b/reframe/core/namespaces.py index 5fcc30640e..eee629a7a0 100644 --- a/reframe/core/namespaces.py +++ b/reframe/core/namespaces.py @@ -10,8 +10,6 @@ import abc -import reframe.core.directives as directives - class LocalNamespace: '''Local namespace of a regression test. From eb35fbeb8f8296943d13d505548a1601c3e635cc Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Fri, 9 Apr 2021 23:54:16 +0200 Subject: [PATCH 7/8] Use `__getattribute__()` to implement the aliasing for directives - Also fix post-merge problems. --- reframe/core/directives.py | 8 +++++--- reframe/core/meta.py | 4 +++- reframe/core/pipeline.py | 10 ++++++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/reframe/core/directives.py b/reframe/core/directives.py index 6ee45b8ae8..fa6a5199a2 100644 --- a/reframe/core/directives.py +++ b/reframe/core/directives.py @@ -53,7 +53,7 @@ def kwargs(self): return self._kwargs def apply(self, obj): - fn = getattr(obj, '_D_' + self.name) + fn = getattr(obj, self.name) fn(*self.args, **self.kwargs) @@ -72,8 +72,10 @@ def add(self, name, *args, **kwargs): def apply(cls, obj): '''Apply all directives of class ``cls`` to the object ``obj``.''' - for d in cls._rfm_dir_registry.directives: - d.apply(obj) + for c in cls.mro(): + if hasattr(c, '_rfm_dir_registry'): + for d in c._rfm_dir_registry.directives: + d.apply(obj) def reset(cls): diff --git a/reframe/core/meta.py b/reframe/core/meta.py index 9f8f2e4760..ad80b5db0d 100644 --- a/reframe/core/meta.py +++ b/reframe/core/meta.py @@ -7,7 +7,9 @@ # Meta-class for creating regression tests. # +import functools +import reframe.core.directives as directives import reframe.core.namespaces as namespaces import reframe.core.parameters as parameters import reframe.core.variables as variables @@ -159,7 +161,7 @@ def __init__(cls, name, bases, namespace, **kwargs): if hasattr(b, '_rfm_pipeline_hooks'): hooks.update(getattr(b, '_rfm_pipeline_hooks')) - cls._rfm_pipeline_hooks = hooks # HookRegistry(local_hooks) + cls._rfm_pipeline_hooks = hooks cls._final_methods = {v.__name__ for v in namespace.values() if hasattr(v, '_rfm_final')} diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index bacd409ccf..00160b37e6 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -21,6 +21,7 @@ import os import shutil +import reframe.core.directives as directives import reframe.core.environments as env import reframe.core.fields as fields import reframe.core.hooks as hooks @@ -772,6 +773,9 @@ def __new__(cls, *args, _rfm_use_params=False, **kwargs): # Initialize the test obj.__rfm_init__(name, prefix) + + # Apply the directives + directives.apply(cls, obj) return obj def __init__(self): @@ -789,11 +793,13 @@ def _add_hooks(cls, stage): pipeline_hooks = cls._rfm_pipeline_hooks fn = getattr(cls, stage) new_fn = hooks.attach_hooks(pipeline_hooks)(fn) - setattr(cls, '_rfm_pipeline_fn_' + stage, new_fn) + setattr(cls, '_P_' + stage, new_fn) def __getattribute__(self, name): if name in _PIPELINE_STAGES: - name = f'_rfm_pipeline_fn_{name}' + name = f'_P_{name}' + elif name in directives.NAMES: + name = f'_D_{name}' return super().__getattribute__(name) From 625de219474428df449168a6720dd1ef3ea2323e Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Sat, 10 Apr 2021 00:29:39 +0200 Subject: [PATCH 8/8] Update documentation + remove unnecessary function --- docs/regression_test_api.rst | 8 ++++---- reframe/core/decorators.py | 2 +- reframe/core/directives.py | 19 +------------------ 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/docs/regression_test_api.rst b/docs/regression_test_api.rst index c92173a008..84cd4b072f 100644 --- a/docs/regression_test_api.rst +++ b/docs/regression_test_api.rst @@ -224,7 +224,7 @@ In essence, these builtins exert control over the test creation, and they allow Directives ---------- -.. versionadded:: 3.5.2 +.. versionadded:: 3.5.3 Directives are special functions that are called at the class level but will be applied to the newly created test. Directives can also be invoked as normal test methods once the test has been created. @@ -273,7 +273,7 @@ Static test information can be defined in the test class body and any adaptation .. deprecated:: 3.3 Passing an integer to the ``how`` argument as well as using the ``subdeps`` argument is deprecated. - .. versionchanged:: 3.5.2 + .. versionchanged:: 3.5.3 This function has become a directive. @@ -285,7 +285,7 @@ Static test information can be defined in the test class body and any adaptation .. versionadded:: 3.5.1 - .. versionchanged:: 3.5.2 + .. versionchanged:: 3.5.3 This function has become a directive. @@ -298,7 +298,7 @@ Static test information can be defined in the test class body and any adaptation .. versionadded:: 3.5.1 - .. versionchanged:: 3.5.2 + .. versionchanged:: 3.5.3 This function has become a directive. diff --git a/reframe/core/decorators.py b/reframe/core/decorators.py index dfa5cd4dd2..f7a4085b9d 100644 --- a/reframe/core/decorators.py +++ b/reframe/core/decorators.py @@ -222,7 +222,7 @@ def _fn(*args, **kwargs): def run_before(stage): - '''Decorator for attaching a test method to a pipeline stage. + '''Decorator for attaching a test method to a pipeline stage. The method will run just before the specified pipeline stage and it should not accept any arguments except ``self``. diff --git a/reframe/core/directives.py b/reframe/core/directives.py index fa6a5199a2..eb76f04ede 100644 --- a/reframe/core/directives.py +++ b/reframe/core/directives.py @@ -15,9 +15,7 @@ A directive simply captures the arguments passed to it and all directives are stored in a registry inside the class. When the final object is created, they will be applied to that instance by calling the target method on the freshly -created object. As soon as they are applied, they set to point to the actual -object method, since they have served their purpose. This allows us to export -functions such as `depends_on()` at the class level. +created object. ''' NAMES = ('depends_on', 'skip', 'skip_if') @@ -76,18 +74,3 @@ def apply(cls, obj): if hasattr(c, '_rfm_dir_registry'): for d in c._rfm_dir_registry.directives: d.apply(obj) - - -def reset(cls): - '''Reset all directives in ``cls`` to point to the corresponding class - methods.''' - - for d in NAMES: - meth_name = '_D_' + d - - # A directive may be defined in a subclass of `RegressionTest`, but - # this will be called on every class that is being created. So we need - # to update the directive only when the target method exists - if hasattr(cls, meth_name): - meth = getattr(cls, meth_name) - setattr(cls, d, meth)