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
65 changes: 56 additions & 9 deletions reframe/core/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def __getitem__(self, key):
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:
Expand Down Expand Up @@ -186,6 +187,7 @@ def __call__(cls, *args, **kwargs):
these would otherwise affect the __init__ method's signature, and these
internal mechanisms must be fully transparent to the user.
'''

obj = cls.__new__(cls, *args, **kwargs)

# Intercept constructor arguments
Expand All @@ -195,15 +197,17 @@ def __call__(cls, *args, **kwargs):
return obj

def __getattr__(cls, name):
''' Attribute lookup method for the MetaNamespace.

This metaclass implements a custom namespace, where built-in `variable`
and `parameter` types are stored in their own sub-namespaces (see
:class:`reframe.core.meta.RegressionTestMeta.MetaNamespace`).
This method will perform an attribute lookup on these sub-namespaces if
a call to the default `__getattribute__` method fails to retrieve the
requested class attribute.
'''Attribute lookup method for the MetaNamespace.

This metaclass uses a custom namespace, where ``variable`` built-in
and ``parameter`` types are stored in their own sub-namespaces (see
:class:`reframe.core.meta.RegressionTestMeta.MetaNamespace`). This
method will perform an attribute lookup on these sub-namespaces if a
call to the default :func:`__getattribute__` method fails to retrieve
the requested class attribute.

'''

try:
return cls._rfm_var_space.vars[name]
except KeyError:
Expand All @@ -214,9 +218,52 @@ def __getattr__(cls, name):
f'class {cls.__qualname__!r} has no attribute {name!r}'
) from None

def __setattr__(cls, name, value):
'''Handle the special treatment required for variables and parameters.

A variable's default value can be updated when accessed as a regular
class attribute. This behaviour does not apply when the assigned value
is a descriptor object. In that case, the task of setting the value is
delegated to the base :func:`__setattr__` (this is to comply with
standard Python behaviour). However, since the variables are already
descriptors which are injected during class instantiation, we disallow
any attempt to override this descriptor (since it would be silently
re-overriden in any case).

Altering the value of a parameter when accessed as a class attribute
is not allowed. This would break the parameter space internals.
'''

# Set the value of a variable (except when the value is a descriptor).
try:
var_space = super().__getattribute__('_rfm_var_space')
if name in var_space:
if not hasattr(value, '__get__'):
var_space[name].define(value)
return
elif not var_space[name].field is value:
desc = '.'.join([cls.__qualname__, name])
raise ValueError(
f'cannot override variable descriptor {desc!r}'
)

except AttributeError:
pass

# Catch attempts to override a test parameter
try:
param_space = super().__getattribute__('_rfm_param_space')
if name in param_space.params:
raise ValueError(f'cannot override parameter {name!r}')

except AttributeError:
pass

super().__setattr__(name, value)

@property
def param_space(cls):
# Make the parameter space available as read-only
''' Make the parameter space available as read-only.'''
return cls._rfm_param_space

def is_abstract(cls):
Expand Down
14 changes: 6 additions & 8 deletions reframe/core/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,20 @@ class TestVar:
'''

__slots__ = (
'field_type', '_default_value', 'name',
'args', 'kwargs', '__attrs__'
'field', '_default_value', 'name', '__attrs__'
)

def __init__(self, *args, **kwargs):
self.field_type = kwargs.pop('field', fields.TypedField)
field_type = kwargs.pop('field', fields.TypedField)
self._default_value = kwargs.pop('value', Undefined)

if not issubclass(self.field_type, fields.Field):
if not issubclass(field_type, fields.Field):
raise ValueError(
f'field {self.field_type!r} is not derived from '
f'field {field_type!r} is not derived from '
f'{fields.Field.__qualname__}'
)

self.args = args
self.kwargs = kwargs
self.field = field_type(*args, **kwargs)
self.__attrs__ = dict()

def is_defined(self):
Expand Down Expand Up @@ -528,7 +526,7 @@ def inject(self, obj, cls):
'''

for name, var in self.items():
setattr(cls, name, var.field_type(*var.args, **var.kwargs))
setattr(cls, name, var.field)
getattr(cls, name).__set_name__(obj, name)

# If the var is defined, set its value
Expand Down
9 changes: 9 additions & 0 deletions unittests/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,12 @@ class MyTest(rfm.RegressionTest):
p = parameter([1, 2, 3])

assert len(MyTest._rfm_local_param_space) == 0


def test_class_attr_access():
class MyTest(rfm.RegressionTest):
p = parameter([1, 2, 3])

assert MyTest.p == (1, 2, 3,)
with pytest.raises(ValueError, match='cannot override parameter'):
MyTest.p = (4, 5,)
20 changes: 20 additions & 0 deletions unittests/test_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,26 @@ class MyTest(rfm.RegressionTest):
v0 = variable(float, value=0.5)


def test_class_attr_access():
class MyTest(rfm.RegressionTest):
v0 = variable(int, value=1)

assert MyTest.v0 == 1
MyTest.v0 = 2
assert MyTest.v0 == 2
MyTest.v0 += 1
assert MyTest.v0 == 3
assert MyTest().v0 == 3

class Descriptor:
'''Dummy descriptor to attempt overriding the variable descriptor.'''
def __get__(self, obj, objtype=None):
return 'dummy descriptor'

with pytest.raises(ValueError, match='cannot override variable descr'):
MyTest.v0 = Descriptor()


def test_double_action_on_variable():
'''Modifying a variable in the class body is permitted.'''
class MyTest(rfm.RegressionTest):
Expand Down