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
12 changes: 8 additions & 4 deletions docs/regression_test_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
32 changes: 32 additions & 0 deletions reframe/core/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 14 additions & 15 deletions reframe/core/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]))
Expand Down
7 changes: 7 additions & 0 deletions unittests/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
12 changes: 12 additions & 0 deletions unittests/test_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.'