@@ -158,13 +158,24 @@ class MyRequiredTest(HelloTest):
158158 from :class:`EchoBaseTest` to throw an error indicating that the variable
159159 ``what`` has not been set.
160160
161+ Finally, variables may alias each other. If a variable is an alias of
162+ another one it behaves in the exact same way as its target. If a change is
163+ made to the target variable, this is reflected to the alias and vice
164+ versa. However, alias variables are independently loggable: an alias may
165+ be logged but not its target and vice versa. Aliased variables are useful
166+ when you want to rename a variable and you want to keep the old one for
167+ compatibility reasons.
168+
161169 :param `types`: the supported types for the variable.
162170 :param value: the default value assigned to the variable. If no value is
163171 provided, the variable is set as ``required``.
164172 :param field: the field validator to be used for this variable. If no
165173 field argument is provided, it defaults to
166174 :attr:`reframe.core.fields.TypedField`. The provided field validator
167175 by this argument must derive from :attr:`reframe.core.fields.Field`.
176+ :param alias: the target variable if this variable is an alias. This must
177+ refer to an already declared variable and neither default value nor a
178+ field can be specified for an alias variable.
168179 :param loggable: Mark this variable as loggable. If :obj:`True`, this
169180 variable will become a log record attribute under the name
170181 ``check_NAME``, where ``NAME`` is the name of the variable.
@@ -175,37 +186,71 @@ class MyRequiredTest(HelloTest):
175186 .. versionadded:: 3.10.2
176187 The ``loggable`` argument is added.
177188
189+ .. versionadded:: 4.0.0
190+ Alias variable are introduced.
191+
178192 '''
179193
180- __slots__ = ('_default_value' , '_field' , '_loggable' , '_name' )
194+ # NOTE: We can't use truly private fields in `__slots__`, because
195+ # `__setattr__()` will be called with their mangled name and we cannot
196+ # match them in the `__slots__` without making implementation-defined
197+ # assumptions about the mangled name. So we just add the `_p_` prefix for
198+ # to denote the "private" fields.
199+
200+ __slots__ = ('_p_default_value' , '_p_field' ,
201+ '_loggable' , '_name' , '_target' )
202+
203+ __mutable_props = ('_default_value' ,)
181204
182205 def __init__ (self , * args , ** kwargs ):
206+ alias = kwargs .pop ('alias' , None )
207+ if alias is not None and 'field' in kwargs :
208+ raise ValueError (f"'field' cannot be set for an alias variable" )
209+
210+ if alias is not None and 'value' in kwargs :
211+ raise ValueError ('alias variables do not accept default values' )
212+
213+ if alias is not None and not isinstance (alias , TestVar ):
214+ raise TypeError (f"'alias' must refer to a variable; "
215+ f"found { type (alias ).__name__ !r} " )
216+
183217 field_type = kwargs .pop ('field' , fields .TypedField )
184- self ._default_value = kwargs .pop ('value' , Undefined )
185- self ._loggable = kwargs .pop ('loggable' , False )
218+ if alias is not None :
219+ self ._p_default_value = alias ._default_value
220+ else :
221+ self ._p_default_value = kwargs .pop ('value' , Undefined )
186222
223+ self ._loggable = kwargs .pop ('loggable' , False )
187224 if not issubclass (field_type , fields .Field ):
188225 raise TypeError (
189226 f'field { field_type !r} is not derived from '
190227 f'{ fields .Field .__qualname__ } '
191228 )
192229
193- self ._field = field_type (* args , ** kwargs )
230+ if alias is not None :
231+ self ._p_field = alias ._field
232+ else :
233+ self ._p_field = field_type (* args , ** kwargs )
234+
235+ self ._target = alias
194236
195237 @classmethod
196238 def create_deprecated (cls , var , message ,
197239 kind = DEPRECATE_RDWR , from_version = '0.0.0' ):
198240 ret = TestVar .__new__ (TestVar )
199- ret ._field = fields .DeprecatedField (var .field , message ,
200- kind , from_version )
201- ret ._default_value = var ._default_value
241+ ret ._p_field = fields .DeprecatedField (var .field , message ,
242+ kind , from_version )
243+ ret ._p_default_value = var ._default_value
202244 ret ._loggable = var ._loggable
245+ ret ._target = var ._target
203246 return ret
204247
205- def _check_deprecation (self , kind ):
206- if isinstance (self .field , fields .DeprecatedField ):
207- if self .field .op & kind :
208- user_deprecation_warning (self .field .message )
248+ def _warn_deprecation (self , kind ):
249+ if self .is_deprecated () and self .field .op & kind :
250+ user_deprecation_warning (self .field .message )
251+
252+ def is_deprecated (self ):
253+ return isinstance (self ._p_field , fields .DeprecatedField )
209254
210255 def is_loggable (self ):
211256 return self ._loggable
@@ -217,29 +262,41 @@ def undefine(self):
217262 self ._default_value = Undefined
218263
219264 def define (self , value ):
220- if value != self ._default_value :
221- # We only issue a deprecation warning if the write attempt changes
222- # the value. This is a workaround to the fact that if a variable
223- # defined in parent classes is accessed by the current class, then
224- # the definition of the variable is "copied" in the class body as
225- # an assignment (see `MetaNamespace.__getitem__()`). The
226- # `VarSpace.extend()` method then checks all local class body
227- # assignments and if they refer to a variable (inherited or not),
228- # they call `define()` on it. So, practically, in this case, the
229- # `_default_value` is set redundantly once per class in the
230- # hierarchy.
231- self ._check_deprecation (DEPRECATE_WR )
232-
265+ self ._warn_deprecation (DEPRECATE_WR )
233266 self ._default_value = value
234267
268+ @property
269+ def _default_value (self ):
270+ if self ._target :
271+ return self ._target ._default_value
272+ else :
273+ return self ._p_default_value
274+
275+ @_default_value .setter
276+ def _default_value (self , value ):
277+ if self ._target :
278+ self ._target ._default_value = value
279+ else :
280+ self ._p_default_value = value
281+
235282 @property
236283 def default_value (self ):
237284 # Variables must be returned by-value to prevent an instance from
238285 # modifying the class variable space.
239286 self ._check_is_defined ()
240- self ._check_deprecation (DEPRECATE_RD )
287+ self ._warn_deprecation (DEPRECATE_RD )
241288 return copy .deepcopy (self ._default_value )
242289
290+ @property
291+ def _field (self ):
292+ if self .is_deprecated ():
293+ return self ._p_field
294+
295+ if self ._target :
296+ return self ._target ._field
297+ else :
298+ return self ._p_field
299+
243300 @property
244301 def field (self ):
245302 return self ._field
@@ -253,14 +310,14 @@ def __set_name__(self, owner, name):
253310
254311 def __setattr__ (self , name , value ):
255312 '''Set any additional variable attribute into the default value.'''
256- if name in self .__slots__ :
313+ if name in self .__slots__ or name in self . __mutable_props :
257314 super ().__setattr__ (name , value )
258315 else :
259316 setattr (self ._default_value , name , value )
260317
261318 def __getattr__ (self , name ):
262319 '''Attribute lookup into the variable's value.'''
263- def_val = self .__getattribute__ ('_default_value ' )
320+ def_val = self .__getattribute__ ('_p_default_value ' )
264321
265322 # NOTE: This if below is necessary to avoid breaking the deepcopy
266323 # of instances of this class. Without it, a deepcopy of instances of
@@ -284,12 +341,26 @@ def _check_is_defined(self):
284341 f'variable { self ._name !r} is not assigned a value'
285342 )
286343
287- def __repr__ (self ):
344+ def __str__ (self ):
288345 self ._check_is_defined ()
289- return repr (self ._default_value )
346+ return str (self ._default_value )
290347
291- def __str__ (self ):
292- return self .__repr__ ()
348+ def __repr__ (self ):
349+ import reframe
350+ if hasattr (reframe , '__build_docs__' ):
351+ return str (self )
352+
353+ try :
354+ name = self .name
355+ except AttributeError :
356+ name = '<undef>'
357+
358+ if self .is_defined ():
359+ value = self ._default_value
360+ else :
361+ value = '<undef>'
362+
363+ return f'TestVar(name={ name !r} , value={ value !r} )'
293364
294365 def __bytes__ (self ):
295366 self ._check_is_defined ()
@@ -593,6 +664,30 @@ def __ceil__(self):
593664 return math .ceil (self ._default_value )
594665
595666
667+ class ShadowVar (TestVar ):
668+ '''A shadow instance of another variable.
669+
670+ This is essentially a fully-fledged shallow copy of another variable. It
671+ is used during the construction of the class namespace to bring in scope a
672+ requested variable that is defined in a base class (see
673+ `MetaNamespace.__getitem__()`)
674+
675+ We could not simply create a reference of the original variable in the
676+ current namespace, because we need a mechanism to differentiate the
677+ lowered variable from any redefinition, which is illegal.
678+
679+ Also, we don't need a deep copy, since the shadow variable will replace
680+ the original variable in the newly constructed `VarSpace`.
681+
682+ '''
683+
684+ def __init__ (self , other ):
685+ for name in self .__slots__ :
686+ setattr (self , name , getattr (other , name ))
687+
688+ self ._warn_deprecation (DEPRECATE_RD )
689+
690+
596691class VarSpace (namespaces .Namespace ):
597692 '''Variable space of a regression test.
598693
@@ -645,8 +740,7 @@ def extend(self, cls):
645740 is disallowed.
646741 '''
647742 local_varspace = getattr (cls , self .local_namespace_name , False )
648- while local_varspace :
649- key , var = local_varspace .popitem ()
743+ for key , var in local_varspace .items ():
650744 if isinstance (var , TestVar ):
651745 # Disable redeclaring a variable
652746 if key in self .vars :
@@ -657,13 +751,19 @@ def extend(self, cls):
657751 # Add a new var
658752 self .vars [key ] = var
659753
754+ local_varspace .clear ()
755+
660756 # If any previously declared variable was defined in the class body
661757 # by directly assigning it a value, retrieve this value from the class
662758 # namespace and update it into the variable space.
663759 _assigned_vars = set ()
664760 for key , value in cls .__dict__ .items ():
665761 if key in self .vars :
666- self .vars [key ].define (value )
762+ if isinstance (value , ShadowVar ):
763+ self .vars [key ] = value
764+ else :
765+ self .vars [key ].define (value )
766+
667767 _assigned_vars .add (key )
668768 elif value is Undefined :
669769 # Cannot be set as Undefined if not a variable
0 commit comments