diff --git a/docs/regression_test_api.rst b/docs/regression_test_api.rst index fdd20e5e18..46f43f6bfa 100644 --- a/docs/regression_test_api.rst +++ b/docs/regression_test_api.rst @@ -59,6 +59,7 @@ In essence, these builtins exert control over the test creation, and they allow class Foo(rfm.RegressionTest): variant = parameter(['A', 'B']) + # print(variant) # Error: a parameter may only be accessed from the class instance. def __init__(self): if self.variant == 'A': @@ -67,8 +68,9 @@ In essence, these builtins exert control over the test creation, and they allow do_other() One of the most powerful features about these built-in functions is that they store their input information at the class level. - This means if one were to extend or specialize an existing regression test, the test attribute additions and modifications made through built-in functions in the parent class will be automatically inherited by the child test. - For instance, continuing with the example above, one could override the :func:`__init__` method in the :class:`MyTest` regression test as follows: + However, a parameter may only be accessed from the class instance and accessing it directly from the class body is disallowed. + With this approach, extending or specializing an existing parametrized regression test becomes straightforward, since the test attribute additions and modifications made through built-in functions in the parent class are automatically inherited by the child test. + For instance, continuing with the example above, one could override the :func:`__init__` method in the :class:`Foo` regression test as follows: .. code:: python @@ -124,7 +126,7 @@ In essence, these builtins exert control over the test creation, and they allow class Foo(rfm.RegressionTest): my_var = variable(int, value=8) - not_a_var = 4 + not_a_var = my_var - 4 def __init__(self): print(self.my_var) # prints 8. @@ -133,13 +135,15 @@ In essence, these builtins exert control over the test creation, and they allow self.my_var = 10 # tests may also assign values the standard way The argument ``value`` in the :func:`variable` built-in sets the default value for the variable. - As mentioned above, a variable may not be declared more than once, but its default value can be updated by simply assigning it a new value directly in the class body. + Note that a variable may be accesed directly from the class body as long as its value was previously assigned in the same class body. + As mentioned above, a variable may not be declared more than once, but its default value can be updated by simply assigning it a new value directly in the class body. However, a variable may only be acted upon once in the same class body. .. code:: python class Bar(Foo): my_var = 4 # my_var = 'override' # Error again! + # my_var = 8 # Error: Double action on `my_var` is not allowed. def __init__(self): print(self.my_var) # prints 4. diff --git a/reframe/core/meta.py b/reframe/core/meta.py index 1b4a71aaca..16058cd8a6 100644 --- a/reframe/core/meta.py +++ b/reframe/core/meta.py @@ -28,6 +28,38 @@ def __setitem__(self, key, value): else: super().__setitem__(key, value) + def __getitem__(self, key): + '''Expose and control access to the local namespaces. + + Variables may only be retrieved if their value has been previously + set. Accessing a parameter in the class body is disallowed (the + actual test parameter is set during the class instantiation). + ''' + try: + return super().__getitem__(key) + except KeyError as err: + try: + # Handle variable access + var = self['_rfm_local_var_space'][key] + if var.is_defined(): + return var.default_value + else: + raise ValueError( + f'variable {key!r} is not assigned a value' + ) + + except KeyError: + # Handle parameter access + if key in self['_rfm_local_param_space']: + raise ValueError( + 'accessing a test parameter from the class ' + 'body is disallowed' + ) + else: + # If 'key' is neither a variable nor a parameter, + # raise the exception from the base __getitem__. + raise err from None + @classmethod def __prepare__(metacls, name, bases, **kwargs): namespace = super().__prepare__(name, bases, **kwargs) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index af67365d7d..41d052dc5c 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -138,9 +138,22 @@ class RegressionMixin(metaclass=RegressionTestMeta): .. versionadded:: 3.4.2 ''' + def __getattribute__(self, name): + try: + return super().__getattribute__(name) + except AttributeError: + # Intercept the AttributeError if the name corresponds to a + # required variable. + if (name in self._rfm_var_space.vars and + not self._rfm_var_space.vars[name].is_defined()): + raise AttributeError( + f'required variable {name!r} has not been set' + ) from None + else: + super().__getattr__(name) -class RegressionTest(jsonext.JSONSerializable, metaclass=RegressionTestMeta): +class RegressionTest(RegressionMixin, jsonext.JSONSerializable): '''Base class for regression tests. All regression tests must eventually inherit from this class. @@ -764,20 +777,6 @@ def __new__(cls, *args, _rfm_use_params=False, **kwargs): def __init__(self): pass - def __getattribute__(self, name): - try: - return super().__getattribute__(name) - except AttributeError: - # Intercept the AttributeError if the name corresponds to a - # required variable. - if (name in self._rfm_var_space.vars and - not self._rfm_var_space.vars[name].is_defined()): - raise AttributeError( - f'required variable {name!r} has not been set' - ) from None - else: - super().__getattr__(name) - def _append_parameters_to_name(self): if self._rfm_param_space.params: return '_' + '_'.join([util.toalphanum(str(self.__dict__[key])) diff --git a/unittests/test_parameters.py b/unittests/test_parameters.py index 4c13386d8f..406ff00cd7 100644 --- a/unittests/test_parameters.py +++ b/unittests/test_parameters.py @@ -228,3 +228,10 @@ class Bar(Base): assert Foo(_rfm_use_params=True).p0.val == -20 assert Bar(_rfm_use_params=True).p0.val == 1 assert Bar(_rfm_use_params=True).p0.val == 2 + + +def test_param_access(): + with pytest.raises(ValueError): + class Foo(rfm.RegressionTest): + p = parameter([1, 2, 3]) + x = f'accessing {p!r} in the class body is disallowed.' diff --git a/unittests/test_variables.py b/unittests/test_variables.py index e678824c23..0c982bf33c 100644 --- a/unittests/test_variables.py +++ b/unittests/test_variables.py @@ -180,3 +180,15 @@ class Bar(Base): assert Foo().my_var == [1, 2, 3] assert Bar().my_var == [1, 2] + + +def test_variable_access(): + class Foo(rfm.RegressionMixin): + my_var = variable(str, value='bananas') + x = f'accessing {my_var!r} works because it has a default value.' + + assert 'bananas' in getattr(Foo, 'x') + with pytest.raises(ValueError): + class Foo(rfm.RegressionMixin): + my_var = variable(int) + x = f'accessing {my_var!r} fails because its value is not set.'