Skip to content

Commit 1c9b188

Browse files
author
Vasileios Karakasis
authored
Merge pull request #1819 from jjotero/feature/improve-error-handling
[feat] Improve access to the namespaces of both parameter and variable built-ins
2 parents 8de06d4 + 6d96511 commit 1c9b188

File tree

7 files changed

+736
-83
lines changed

7 files changed

+736
-83
lines changed

reframe/core/meta.py

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,40 @@
1717
class RegressionTestMeta(type):
1818

1919
class MetaNamespace(namespaces.LocalNamespace):
20-
'''Custom namespace to control the cls attribute assignment.'''
20+
'''Custom namespace to control the cls attribute assignment.
21+
22+
Regular Python class attributes can be overriden by either
23+
parameters or variables respecting the order of execution.
24+
A variable or a parameter may not be declared more than once in the
25+
same class body. Overriding a variable with a parameter or the other
26+
way around has an undefined behaviour. A variable's value may be
27+
updated multiple times within the same class body. A parameter's
28+
value may not be updated more than once within the same class body.
29+
'''
30+
2131
def __setitem__(self, key, value):
22-
if isinstance(value, variables.VarDirective):
32+
if isinstance(value, variables.TestVar):
2333
# Insert the attribute in the variable namespace
2434
self['_rfm_local_var_space'][key] = value
35+
36+
# Override the regular class attribute (if present)
37+
self._namespace.pop(key, None)
38+
2539
elif isinstance(value, parameters.TestParam):
2640
# Insert the attribute in the parameter namespace
2741
self['_rfm_local_param_space'][key] = value
42+
43+
# Override the regular class attribute (if present)
44+
self._namespace.pop(key, None)
45+
46+
elif key in self['_rfm_local_param_space']:
47+
raise ValueError(
48+
f'cannot override parameter {key!r}'
49+
)
2850
else:
29-
super().__setitem__(key, value)
51+
# Insert the items manually to overide the namespace clash
52+
# check from the base namespace.
53+
self._namespace[key] = value
3054

3155
def __getitem__(self, key):
3256
'''Expose and control access to the local namespaces.
@@ -40,22 +64,31 @@ def __getitem__(self, key):
4064
except KeyError as err:
4165
try:
4266
# Handle variable access
43-
var = self['_rfm_local_var_space'][key]
44-
if var.is_defined():
45-
return var.default_value
46-
else:
47-
raise ValueError(
48-
f'variable {key!r} is not assigned a value'
49-
)
67+
v = self['_rfm_local_var_space'][key]
68+
v.__set_name__(self, key)
69+
return v
5070

5171
except KeyError:
5272
# Handle parameter access
5373
if key in self['_rfm_local_param_space']:
5474
raise ValueError(
5575
'accessing a test parameter from the class '
5676
'body is disallowed'
57-
)
77+
) from None
5878
else:
79+
# As the last resource, look if key is a variable in
80+
# any of the base classes. If so, make its value
81+
# available in the current class' namespace.
82+
for b in self['_rfm_bases']:
83+
if key in b._rfm_var_space:
84+
v = b._rfm_var_space[key]
85+
v.__set_name__(self, key)
86+
87+
# Store a deep-copy of the variable's
88+
# value and return.
89+
self._namespace[key] = v.default_value
90+
return self._namespace[key]
91+
5992
# If 'key' is neither a variable nor a parameter,
6093
# raise the exception from the base __getitem__.
6194
raise err from None
@@ -64,6 +97,11 @@ def __getitem__(self, key):
6497
def __prepare__(metacls, name, bases, **kwargs):
6598
namespace = super().__prepare__(name, bases, **kwargs)
6699

100+
# Keep reference to the bases inside the namespace
101+
namespace['_rfm_bases'] = [
102+
b for b in bases if hasattr(b, '_rfm_var_space')
103+
]
104+
67105
# Regression test parameter space defined at the class level
68106
local_param_space = namespaces.LocalNamespace()
69107
namespace['_rfm_local_param_space'] = local_param_space
@@ -78,7 +116,7 @@ def __prepare__(metacls, name, bases, **kwargs):
78116

79117
# Directives to add/modify a regression test variable
80118
namespace['variable'] = variables.TestVar
81-
namespace['required'] = variables.UndefineVar()
119+
namespace['required'] = variables.Undefined
82120
return metacls.MetaNamespace(namespace)
83121

84122
def __new__(metacls, name, bases, namespace, **kwargs):
@@ -172,7 +210,7 @@ def __call__(cls, *args, **kwargs):
172210
obj.__init__(*args, **kwargs)
173211
return obj
174212

175-
def __getattribute__(cls, name):
213+
def __getattr__(cls, name):
176214
''' Attribute lookup method for the MetaNamespace.
177215
178216
This metaclass implements a custom namespace, where built-in `variable`
@@ -183,15 +221,14 @@ def __getattribute__(cls, name):
183221
requested class attribute.
184222
'''
185223
try:
186-
return super().__getattribute__(name)
187-
except AttributeError:
224+
return cls._rfm_var_space.vars[name]
225+
except KeyError:
188226
try:
189-
return cls._rfm_local_var_space[name]
227+
return cls._rfm_param_space.params[name]
190228
except KeyError:
191-
try:
192-
return cls._rfm_local_param_space[name]
193-
except KeyError:
194-
return super().__getattr__(name)
229+
raise AttributeError(
230+
f'class {cls.__qualname__!r} has no attribute {name!r}'
231+
) from None
195232

196233
@property
197234
def param_space(cls):

reframe/core/namespaces.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,11 @@ def _raise_namespace_clash(self, name):
6363
f'{name!r} is already present in the current namespace'
6464
)
6565

66+
def clear(self):
67+
self._namespace = {}
68+
6669

67-
class Namespace(metaclass=abc.ABCMeta):
70+
class Namespace(LocalNamespace, metaclass=abc.ABCMeta):
6871
'''Namespace of a regression test.
6972
7073
The final namespace may be built by inheriting namespaces from
@@ -111,7 +114,7 @@ def namespace_name(self):
111114
'''
112115

113116
def __init__(self, target_cls=None, illegal_names=None):
114-
self._namespace = {}
117+
super().__init__()
115118
if target_cls:
116119
# Assert the Namespace can be built for the target_cls
117120
self.assert_target_cls(target_cls)
@@ -173,5 +176,7 @@ def inject(self, obj, objtype=None):
173176
``obj``.
174177
'''
175178

176-
def items(self):
177-
return self._namespace.items()
179+
def __setitem__(self, key, value):
180+
raise ValueError(
181+
f'cannot set item {key!r} into a {type(self).__qualname__} object'
182+
)

reframe/core/parameters.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ def join(self, other, cls):
124124
def extend(self, cls):
125125
'''Extend the parameter space with the local parameter space.'''
126126

127-
for name, p in cls._rfm_local_param_space.items():
127+
local_param_space = getattr(cls, self.local_namespace_name)
128+
for name, p in local_param_space.items():
128129
self.params[name] = (
129130
p.filter_params(self.params.get(name, ())) + p.values
130131
)
@@ -139,6 +140,9 @@ def extend(self, cls):
139140
f'parameter type'
140141
)
141142

143+
# Clear the local param space
144+
local_param_space.clear()
145+
142146
def inject(self, obj, cls=None, use_params=False):
143147
'''Insert the params in the regression test.
144148

reframe/core/pipeline.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -149,19 +149,17 @@ class RegressionMixin(metaclass=RegressionTestMeta):
149149
.. versionadded:: 3.4.2
150150
'''
151151

152-
def __getattribute__(self, name):
153-
try:
154-
return super().__getattribute__(name)
155-
except AttributeError:
156-
# Intercept the AttributeError if the name corresponds to a
157-
# required variable.
158-
if (name in self._rfm_var_space.vars and
159-
not self._rfm_var_space.vars[name].is_defined()):
160-
raise AttributeError(
161-
f'required variable {name!r} has not been set'
162-
) from None
163-
else:
164-
super().__getattr__(name)
152+
def __getattr__(self, name):
153+
''' Intercept the AttributeError if the name is a required variable.'''
154+
if (name in self._rfm_var_space and
155+
not self._rfm_var_space[name].is_defined()):
156+
raise AttributeError(
157+
f'required variable {name!r} has not been set'
158+
) from None
159+
else:
160+
raise AttributeError(
161+
f'{type(self).__qualname__} object has no attribute {name!r}'
162+
)
165163

166164

167165
class RegressionTest(RegressionMixin, jsonext.JSONSerializable):

0 commit comments

Comments
 (0)