diff --git a/reframe/core/variables.py b/reframe/core/variables.py index c8777503ce..e25fd7f484 100644 --- a/reframe/core/variables.py +++ b/reframe/core/variables.py @@ -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) @@ -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 @@ -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): @@ -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) diff --git a/unittests/test_variables.py b/unittests/test_variables.py index fa86fdadad..6caf02a922 100644 --- a/unittests/test_variables.py +++ b/unittests/test_variables.py @@ -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():