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
54 changes: 29 additions & 25 deletions reframe/core/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ class TestVar:
:meta private:
'''

__slots__ = (
'field', '_default_value', 'name', '__attrs__'
)
__slots__ = ('_field', '_default_value', '_name',)

def __init__(self, *args, **kwargs):
field_type = kwargs.pop('field', fields.TypedField)
Expand All @@ -53,8 +51,7 @@ def __init__(self, *args, **kwargs):
f'{fields.Field.__qualname__}'
)

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

def is_defined(self):
return self._default_value is not Undefined
Expand All @@ -73,35 +70,47 @@ def default_value(self):
return copy.deepcopy(self._default_value)

@property
def attrs(self):
# Variable attributes must also be returned by-value.
return copy.deepcopy(self.__attrs__)
def field(self):
return self._field

@property
def name(self):
return self._name

def __set_name__(self, owner, name):
self.name = name
self._name = name

def __setattr__(self, name, value):
'''Set any additional variable attribute into __attrs__.'''
if name in self.__slots__:
super().__setattr__(name, value)
else:
self.__attrs__[name] = value
setattr(self._default_value, name, value)

def __getattr__(self, name):
'''Attribute lookup into __attrs__.'''
attrs = self.__getattribute__('__attrs__')
try:
return attrs[name]
except KeyError:
var_name = self.__getattribute__('name')
raise AttributeError(
f'variable {var_name!r} has no attribute {name!r}'
) from None
'''Attribute lookup into the variable's value.'''
def_val = self.__getattribute__('_default_value')

# NOTE: This if below is necessary to avoid breaking the deepcopy
# of instances of this class. Without it, a deepcopy of instances of
# this class can return an instance of _UndefinedType when def_val
# is Undefined. This is because _UndefinedType implements a custom
# __deepcopy__ method.
if def_val is not Undefined:
try:
return getattr(def_val, name)
except AttributeError:
'''Raise the AttributeError below.'''

var_name = self.__getattribute__('_name')
raise AttributeError(
f'variable {var_name!r} has no attribute {name!r}'
) from None

def _check_is_defined(self):
if not self.is_defined():
raise ReframeSyntaxError(
f'variable {self.name} is not assigned a value'
f'variable {self._name} is not assigned a value'
)

def __repr__(self):
Expand Down Expand Up @@ -538,11 +547,6 @@ def inject(self, obj, cls):
if var.is_defined():
setattr(obj, name, var.default_value)

# If the variable value itself has attributes, inject them.
value = getattr(obj, name)
for attr, attr_value in var.attrs.items():
setattr(value, attr, attr_value)

# Track the variables that have been injected.
self._injected_vars.add(name)

Expand Down
5 changes: 5 additions & 0 deletions unittests/test_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,12 @@ class MyTest(rfm.RegressionTest):
v = variable(Foo, value=Foo())
v.my_attr = 'Injected attribute'

class OtherTest(MyTest):
assert v.my_attr == 'Injected attribute'
v = Foo()

assert MyTest().v.my_attr == 'Injected attribute'
assert not hasattr(OtherTest().v, 'my_attr')


def test_local_varspace_is_empty():
Expand Down