Skip to content

Commit 8395b49

Browse files
authored
Merge pull request reframe-hpc#3115 from vkarak/feat/vars-implicit-conversions
[feat] Enable default value type checking and implicit conversions at the variable declaration level
2 parents e7e93f0 + 317fef3 commit 8395b49

File tree

8 files changed

+50
-29
lines changed

8 files changed

+50
-29
lines changed

docs/regression_test_api.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ These are called *builtins* because they are directly available for use inside t
4141
However, almost all of these builtins are also available from the :obj:`reframe.core.builtins` module.
4242
The use of this module is required only when creating new tests programmatically using the :func:`~reframe.core.meta.make_test` function.
4343

44+
.. versionchanged:: 3.7.0
45+
Expose :func:`@deferrable <reframe.core.builtins.deferrable>` as a builtin.
46+
47+
.. versionchanged:: 3.11.0
48+
Builtins are now available also through the :obj:`reframe.core.builtins` module.
49+
50+
4451
.. py:method:: reframe.core.pipeline.RegressionMixin.bind(func, name=None)
4552
4653
Bind a free function to a regression test.
@@ -80,13 +87,6 @@ The use of this module is required only when creating new tests programmatically
8087
.. autofunction:: reframe.core.builtins.variable
8188

8289

83-
.. versionchanged:: 3.7.0
84-
Expose :func:`@deferrable <reframe.core.builtins.deferrable>` as a builtin.
85-
86-
.. versionchanged:: 3.11.0
87-
Builtins are now available also through the :obj:`reframe.core.builtins` module.
88-
89-
9090
.. _pipeline-hooks:
9191

9292
--------------

reframe/core/buildsystems.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -981,4 +981,4 @@ def __set__(self, obj, value):
981981
except KeyError:
982982
raise ValueError(f'unknown build system: {value}') from None
983983

984-
super().__set__(obj, value)
984+
return super().__set__(obj, value)

reframe/core/containers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,4 @@ def __set__(self, obj, value):
287287
if isinstance(value, str):
288288
value = ContainerPlatform.create(value)
289289

290-
super().__set__(obj, value)
290+
return super().__set__(obj, value)

reframe/core/decorators.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ def instantiate_all(self, reset_sysenv=0, external_vars=None):
123123
# candidate tests; the leaf tests are consumed at the end of the
124124
# traversal and all instantiated tests (including fixtures) are stored
125125
# in `final_tests`.
126-
unset_vars = {}
127126
final_tests = []
128127
fixture_registry = FixtureRegistry()
129128
while leaf_tests:

reframe/core/fields.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,16 @@ def __get__(self, obj, objtype):
5656
(objtype.__name__, self._name)) from None
5757

5858
def __set__(self, obj, value):
59-
obj.__dict__[self._name] = remove_convertible(value)
59+
# NOTE: We extend the Python data model by returning the value that
60+
# would be otherwise written to the field, if ``obj`` is :obj:`None`.
61+
# Subclasses must make sure to return to the caller the value returned
62+
# from ``super().__set__(...)``.
63+
64+
value = remove_convertible(value)
65+
if obj is None:
66+
return value
67+
68+
obj.__dict__[self._name] = value
6069

6170

6271
class TypedField(Field):
@@ -79,7 +88,7 @@ def _check_type(self, value):
7988
if not any(isinstance(value, t) for t in self._types):
8089
typedescr = '|'.join(t.__name__ for t in self._types)
8190
raise TypeError(
82-
"failed to set field '%s': '%s' is not of type '%s'" %
91+
"failed to set variable '%s': '%s' is not of type '%s'" %
8392
(self._name, value, typedescr))
8493

8594
def __set__(self, obj, value):
@@ -100,18 +109,17 @@ def __set__(self, obj, value):
100109
except TypeError:
101110
continue
102111
else:
103-
super().__set__(obj, value)
104-
return
112+
return super().__set__(obj, value)
105113

106114
# Conversion failed
107115
typenames = [t.__name__ for t in self._types]
108116
raise TypeError(
109-
f'failed to set field {self._name!r}: '
117+
f'failed to set variable {self._name!r}: '
110118
f'could not convert to any of the supported types: '
111119
f'{typenames}'
112120
)
113121
else:
114-
super().__set__(obj, value)
122+
return super().__set__(obj, value)
115123

116124

117125
class ConstantField(Field):
@@ -152,7 +160,7 @@ def __set__(self, obj, value):
152160
if not isinstance(value, ScopedDict):
153161
value = ScopedDict(value) if value is not None else value
154162

155-
Field.__set__(self, obj, value)
163+
return Field.__set__(self, obj, value)
156164

157165

158166
class DeprecatedField(Field):
@@ -187,7 +195,7 @@ def __set__(self, obj, value):
187195
if self._op & DeprecatedField.OP_SET:
188196
user_deprecation_warning(self._message, self._from_version)
189197

190-
self._target_field.__set__(obj, value)
198+
return self._target_field.__set__(obj, value)
191199

192200
def __get__(self, obj, objtype):
193201
if self._op & DeprecatedField.OP_GET:

reframe/core/variables.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,10 @@ def _default_value(self):
343343
def _default_value(self, value):
344344
if self.is_alias():
345345
self._target._default_value = value
346-
else:
346+
elif value is Undefined:
347347
self._p_default_value = value
348+
else:
349+
self._p_default_value = self._p_field.__set__(None, value)
348350

349351
@property
350352
def default_value(self):
@@ -386,6 +388,18 @@ def reset_target(self, new_target):
386388

387389
def __set_name__(self, owner, name):
388390
self._name = name
391+
self._p_field.__set_name__(owner, name)
392+
393+
# Type check and convert the variable's value if defined
394+
if self.is_defined():
395+
if isinstance(self._p_default_value, TestVar):
396+
# Treat shadow variables
397+
value = self._p_default_value._p_default_value
398+
else:
399+
value = self._p_default_value
400+
401+
with suppress_deprecations():
402+
self._p_default_value = self._p_field.__set__(None, value)
389403

390404
def __setattr__(self, name, value):
391405
'''Set any additional variable attribute into the default value.'''
@@ -918,12 +932,16 @@ def inject(self, obj, cls):
918932

919933
def _inject(self, obj, cls):
920934
for name, var in self.items():
935+
# Replace the variable with its descriptor
921936
setattr(cls, name, var.field)
922937
getattr(cls, name).__set_name__(obj, name)
923938

924939
# If the var is defined, set its value
925940
if var.is_defined():
926-
setattr(obj, name, var.default_value)
941+
# Variable's value is already validated and converted,
942+
# so we bypass completely the descriptor logic by not calling
943+
# `setattr()`
944+
obj.__dict__[name] = var.default_value
927945

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

unittests/test_pipeline.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,11 +1607,9 @@ def test_reference_deferrable(dummy_perftest):
16071607
with pytest.raises(TypeError):
16081608
dummy_perftest.reference = {'*': {'value1': (sn.defer(1), -0.1, -0.1)}}
16091609

1610-
class T(rfm.RegressionTest):
1611-
reference = {'*': {'value1': (sn.defer(1), -0.1, -0.1)}}
1612-
16131610
with pytest.raises(TypeError):
1614-
T()
1611+
class T(rfm.RegressionTest):
1612+
reference = {'*': {'value1': (sn.defer(1), -0.1, -0.1)}}
16151613

16161614

16171615
def test_performance_invalid_value(dummytest, sanity_file,

unittests/test_variables.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,9 @@ class MyTest(OneVarTest):
146146

147147

148148
def test_var_type(OneVarTest):
149-
class MyTest(OneVarTest):
150-
foo = 'bananas'
151-
152149
with pytest.raises(TypeError):
153-
MyTest()
150+
class MyTest(OneVarTest):
151+
foo = 'bananas'
154152

155153

156154
def test_require_var(OneVarTest):
@@ -358,7 +356,7 @@ class A(rfm.RegressionTest):
358356
v = variable(int, value=4)
359357
assert (v / 3) == 4/3
360358
assert (3 / v) == 3/4
361-
v /= 2
359+
v //= 2
362360
assert v == 2
363361

364362

0 commit comments

Comments
 (0)