@@ -172,16 +172,40 @@ class MyRequiredTest(HelloTest):
172172 the field validator.
173173 :returns: A new test variable.
174174
175- .. versionadded :: 3.10.2
175+ .. version added :: 3.10.2
176176 The ``loggable`` argument is added.
177177
178178 '''
179179
180- __slots__ = ('_default_value' , '_field' , '_loggable' , '_name' )
180+ # NOTE: We can't use truly private fields in __slots__, because
181+ # __setattr__() will be called with their mangled name and we cannot match
182+ # them in the __slots__ with making implementation-defined assumptions
183+ # about the mangled name. So we just add the `_p_` prefix for "private"
184+
185+ __slots__ = ('_p_default_value' , '_p_field' ,
186+ '_loggable' , '_name' , '_target' )
187+
188+ __mutable_props = ('_default_value' ,)
181189
182190 def __init__ (self , * args , ** kwargs ):
183191 field_type = kwargs .pop ('field' , fields .TypedField )
184- self ._default_value = kwargs .pop ('value' , Undefined )
192+ alias = kwargs .pop ('alias' , None )
193+
194+ if alias and not isinstance (alias , TestVar ):
195+ raise TypeError (f"'alias' must refer to a variable; "
196+ f"found { type (alias ).__name__ !r} " )
197+
198+ if alias and 'field' in kwargs :
199+ raise ValueError (f"'field' cannot be set for an alias variable" )
200+
201+ if alias and 'value' in kwargs :
202+ raise ValueError ('alias variables do not accept default values' )
203+
204+ if alias :
205+ self ._p_default_value = alias ._default_value
206+ else :
207+ self ._p_default_value = kwargs .pop ('value' , Undefined )
208+
185209 self ._loggable = kwargs .pop ('loggable' , False )
186210
187211 if not issubclass (field_type , fields .Field ):
@@ -190,16 +214,22 @@ def __init__(self, *args, **kwargs):
190214 f'{ fields .Field .__qualname__ } '
191215 )
192216
193- self ._field = field_type (* args , ** kwargs )
217+ if alias :
218+ self ._p_field = alias ._field
219+ else :
220+ self ._p_field = field_type (* args , ** kwargs )
221+
222+ self ._target = alias
194223
195224 @classmethod
196225 def create_deprecated (cls , var , message ,
197226 kind = DEPRECATE_RDWR , from_version = '0.0.0' ):
198227 ret = TestVar .__new__ (TestVar )
199- ret ._field = fields .DeprecatedField (var .field , message ,
200- kind , from_version )
201- ret ._default_value = var ._default_value
228+ ret ._p_field = fields .DeprecatedField (var .field , message ,
229+ kind , from_version )
230+ ret ._p_default_value = var ._default_value
202231 ret ._loggable = var ._loggable
232+ ret ._target = var ._target
203233 return ret
204234
205235 def _check_deprecation (self , kind ):
@@ -217,21 +247,23 @@ def undefine(self):
217247 self ._default_value = Undefined
218248
219249 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-
250+ self ._check_deprecation (DEPRECATE_WR )
233251 self ._default_value = value
234252
253+ @property
254+ def _default_value (self ):
255+ if self ._target :
256+ return self ._target ._default_value
257+ else :
258+ return self ._p_default_value
259+
260+ @_default_value .setter
261+ def _default_value (self , value ):
262+ if self ._target :
263+ self ._target ._default_value = value
264+ else :
265+ self ._p_default_value = value
266+
235267 @property
236268 def default_value (self ):
237269 # Variables must be returned by-value to prevent an instance from
@@ -240,6 +272,13 @@ def default_value(self):
240272 self ._check_deprecation (DEPRECATE_RD )
241273 return copy .deepcopy (self ._default_value )
242274
275+ @property
276+ def _field (self ):
277+ if self ._target :
278+ return self ._target ._field
279+ else :
280+ return self ._p_field
281+
243282 @property
244283 def field (self ):
245284 return self ._field
@@ -253,14 +292,14 @@ def __set_name__(self, owner, name):
253292
254293 def __setattr__ (self , name , value ):
255294 '''Set any additional variable attribute into the default value.'''
256- if name in self .__slots__ :
295+ if name in self .__slots__ or name in self . __mutable_props :
257296 super ().__setattr__ (name , value )
258297 else :
259298 setattr (self ._default_value , name , value )
260299
261300 def __getattr__ (self , name ):
262301 '''Attribute lookup into the variable's value.'''
263- def_val = self .__getattribute__ ('_default_value ' )
302+ def_val = self .__getattribute__ ('_p_default_value ' )
264303
265304 # NOTE: This if below is necessary to avoid breaking the deepcopy
266305 # of instances of this class. Without it, a deepcopy of instances of
@@ -284,12 +323,22 @@ def _check_is_defined(self):
284323 f'variable { self ._name !r} is not assigned a value'
285324 )
286325
287- def __repr__ (self ):
326+ def __str__ (self ):
288327 self ._check_is_defined ()
289- return repr (self ._default_value )
328+ return str (self ._default_value )
290329
291- def __str__ (self ):
292- return self .__repr__ ()
330+ def __repr__ (self ):
331+ try :
332+ name = self .name
333+ except AttributeError :
334+ name = '<undef>'
335+
336+ if self .is_defined ():
337+ value = self ._default_value
338+ else :
339+ value = '<undef>'
340+
341+ return f'TestVar(name={ name !r} , value={ value !r} )'
293342
294343 def __bytes__ (self ):
295344 self ._check_is_defined ()
@@ -593,6 +642,30 @@ def __ceil__(self):
593642 return math .ceil (self ._default_value )
594643
595644
645+ class ShadowVar (TestVar ):
646+ '''A shadow instance of another variable.
647+
648+ This is essentially a fully-fledged shallow copy of another variable. It
649+ is used during the construction of the class namespace to bring in scope a
650+ requested variable that is defined in a base class (see
651+ `MetaNamespace.__getitem__()`)
652+
653+ We could not simply create a reference of the original variable in the
654+ current namespace, because we need a mechanism to differentiate the
655+ lowered variable from any redefinition, which is illegal.
656+
657+ Also, we don't need a deep copy, since the shadow variable will replace
658+ the original variable in the newly constructed `VarSpace`.
659+
660+ '''
661+
662+ def __init__ (self , other ):
663+ for name in self .__slots__ :
664+ setattr (self , name , getattr (other , name ))
665+
666+ self ._check_deprecation (DEPRECATE_RD )
667+
668+
596669class VarSpace (namespaces .Namespace ):
597670 '''Variable space of a regression test.
598671
@@ -645,8 +718,7 @@ def extend(self, cls):
645718 is disallowed.
646719 '''
647720 local_varspace = getattr (cls , self .local_namespace_name , False )
648- while local_varspace :
649- key , var = local_varspace .popitem ()
721+ for key , var in local_varspace .items ():
650722 if isinstance (var , TestVar ):
651723 # Disable redeclaring a variable
652724 if key in self .vars :
@@ -657,13 +729,19 @@ def extend(self, cls):
657729 # Add a new var
658730 self .vars [key ] = var
659731
732+ local_varspace .clear ()
733+
660734 # If any previously declared variable was defined in the class body
661735 # by directly assigning it a value, retrieve this value from the class
662736 # namespace and update it into the variable space.
663737 _assigned_vars = set ()
664738 for key , value in cls .__dict__ .items ():
665739 if key in self .vars :
666- self .vars [key ].define (value )
740+ if isinstance (value , ShadowVar ):
741+ self .vars [key ] = value
742+ else :
743+ self .vars [key ].define (value )
744+
667745 _assigned_vars .add (key )
668746 elif value is Undefined :
669747 # Cannot be set as Undefined if not a variable
0 commit comments