Skip to content

Commit 80892c0

Browse files
authored
Merge branch 'master' into modifiable-add-option
2 parents 66d0f8f + 955053c commit 80892c0

File tree

14 files changed

+161
-77
lines changed

14 files changed

+161
-77
lines changed

CHANGES.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
102102
- AddOption and the internal add_local_option which AddOption calls now
103103
recognize a "settable" keyword argument to indicate a project-added
104104
option can also be modified using SetOption. Fixes #3983.
105+
SCons.Environment to SCons.Util to avoid the chance of import loops.
106+
Variables and Environment both use the routine and Environment() uses
107+
a Variables() object so better to move to a safer location.
108+
- ListVariable now has a separate validator, with the functionality
109+
that was previously part of the converter. The main effect is to
110+
allow a developer to supply a custom validator, which previously
111+
could be inhibited by the converter failing before the validator
112+
is reached.
105113

106114

107115
RELEASE 4.7.0 - Sun, 17 Mar 2024 17:22:20 -0700

RELEASE.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ CHANGED/ENHANCED EXISTING FUNCTIONALITY
5656
- AddOption and the internal add_local_option which AddOption calls now
5757
recognize a "settable" keyword argument to indicate a project-added
5858
option can also be modified using SetOption.
59+
- ListVariable now has a separate validator, with the functionality
60+
that was previously part of the converter. The main effect is to
61+
allow a developer to supply a custom validator, which previously
62+
could be inhibited by the converter failing before the validator
63+
is reached.
5964

6065
FIXES
6166
-----

SCons/Variables/ListVariable.py

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@
2323

2424
"""Variable type for List Variables.
2525
26-
A list variable may given as 'all', 'none' or a list of names
27-
separated by comma. After the variable has been processed, the variable
28-
value holds either the named list elements, all list elements or no
29-
list elements at all.
26+
A list variable allows selecting one or more from a supplied set of
27+
allowable values, as well as from an optional mapping of alternate names
28+
(such as aliases and abbreviations) and the special names ``'all'`` and
29+
``'none'``. Specified values are converted during processing into values
30+
only from the allowable values set.
3031
3132
Usage example::
3233
@@ -63,12 +64,18 @@
6364
class _ListVariable(collections.UserList):
6465
"""Internal class holding the data for a List Variable.
6566
66-
The initializer accepts two arguments, the list of actual values
67-
given, and the list of allowable values. Not normally instantiated
68-
by hand, but rather by the ListVariable converter function.
67+
This is normally not directly instantiated, rather the ListVariable
68+
converter callback "converts" string input (or the default value
69+
if none) into an instance and stores it.
70+
71+
Args:
72+
initlist: the list of actual values given.
73+
allowedElems: the list of allowable values.
6974
"""
7075

71-
def __init__(self, initlist=None, allowedElems=None) -> None:
76+
def __init__(
77+
self, initlist: Optional[list] = None, allowedElems: Optional[list] = None
78+
) -> None:
7279
if initlist is None:
7380
initlist = []
7481
if allowedElems is None:
@@ -106,26 +113,60 @@ def prepare_to_store(self):
106113
return str(self)
107114

108115
def _converter(val, allowedElems, mapdict) -> _ListVariable:
109-
"""Convert list variables."""
116+
"""Callback to convert list variables into a suitable form.
117+
118+
The arguments *allowedElems* and *mapdict* are non-standard
119+
for a :class:`Variables` converter: the lambda in the
120+
:func:`ListVariable` function arranges for us to be called correctly.
121+
"""
110122
if val == 'none':
111123
val = []
112124
elif val == 'all':
113125
val = allowedElems
114126
else:
115127
val = [_f for _f in val.split(',') if _f]
116128
val = [mapdict.get(v, v) for v in val]
117-
notAllowed = [v for v in val if v not in allowedElems]
118-
if notAllowed:
119-
raise ValueError(
120-
f"Invalid value(s) for option: {','.join(notAllowed)}"
121-
)
122129
return _ListVariable(val, allowedElems)
123130

124131

125-
# def _validator(key, val, env) -> None:
126-
# """ """
127-
# # TODO: write validator for list variable
128-
# pass
132+
def _validator(key, val, env) -> None:
133+
"""Callback to validate supplied value(s) for a ListVariable.
134+
135+
Validation means "is *val* in the allowed list"? *val* has
136+
been subject to substitution before the validator is called. The
137+
converter created a :class:`_ListVariable` container which is stored
138+
in *env* after it runs; this includes the allowable elements list.
139+
Substitution makes a string made out of the values (only),
140+
so we need to fish the allowed elements list out of the environment
141+
to complete the validation.
142+
143+
Note that since 18b45e456, whether or not ``subst`` has been
144+
called is conditional on the value of the *subst* argument to
145+
:meth:`~SCons.Variables.Variables.Add`, so we have to account for
146+
possible different types of *val*.
147+
148+
Raises:
149+
UserError: if validation failed.
150+
151+
.. versionadded:: 4.8.0
152+
``_validator`` split off from :func:`_converter` with an additional
153+
check for whether *val* has been substituted before the call.
154+
"""
155+
allowedElems = env[key].allowedElems
156+
if isinstance(val, _ListVariable): # not substituted, use .data
157+
notAllowed = [v for v in val.data if v not in allowedElems]
158+
else: # val will be a string
159+
notAllowed = [v for v in val.split() if v not in allowedElems]
160+
if notAllowed:
161+
# Converter only synthesized 'all' and 'none', they are never
162+
# in the allowed list, so we need to add those to the error message
163+
# (as is done for the help msg).
164+
valid = ','.join(allowedElems + ['all', 'none'])
165+
msg = (
166+
f"Invalid value(s) for variable {key!r}: {','.join(notAllowed)!r}. "
167+
f"Valid values are: {valid}"
168+
)
169+
raise SCons.Errors.UserError(msg) from None
129170

130171

131172
# lint: W0622: Redefining built-in 'help' (redefined-builtin)
@@ -136,6 +177,7 @@ def ListVariable(
136177
default: Union[str, List[str]],
137178
names: List[str],
138179
map: Optional[dict] = None,
180+
validator: Optional[Callable] = None,
139181
) -> Tuple[str, str, str, None, Callable]:
140182
"""Return a tuple describing a list variable.
141183
@@ -149,25 +191,35 @@ def ListVariable(
149191
the allowable values (not including any extra names from *map*).
150192
default: the default value(s) for the list variable. Can be
151193
given as string (possibly comma-separated), or as a list of strings.
152-
``all`` or ``none`` are allowed as *default*.
194+
``all`` or ``none`` are allowed as *default*. You can also simulate
195+
a must-specify ListVariable by giving a *default* that is not part
196+
of *names*, it will fail validation if not supplied.
153197
names: the allowable values. Must be a list of strings.
154198
map: optional dictionary to map alternative names to the ones in
155199
*names*, providing a form of alias. The converter will make
156200
the replacement, names from *map* are not stored and will
157201
not appear in the help message.
202+
validator: optional callback to validate supplied values.
203+
The default validator is used if not specified.
158204
159205
Returns:
160206
A tuple including the correct converter and validator. The
161207
result is usable as input to :meth:`~SCons.Variables.Variables.Add`.
208+
209+
.. versionchanged:: 4.8.0
210+
The validation step was split from the converter to allow for
211+
custom validators. The *validator* keyword argument was added.
162212
"""
163213
if map is None:
164214
map = {}
215+
if validator is None:
216+
validator = _validator
165217
names_str = f"allowed names: {' '.join(names)}"
166218
if SCons.Util.is_List(default):
167219
default = ','.join(default)
168220
help = '\n '.join(
169221
(help, '(all|none|comma-separated list of names)', names_str))
170-
return key, help, default, None, lambda val: _converter(val, names, map)
222+
return key, help, default, validator, lambda val: _converter(val, names, map)
171223

172224
# Local Variables:
173225
# tab-width:4

SCons/Variables/ListVariableTests.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def test_ListVariable(self) -> None:
3838
assert o.key == 'test', o.key
3939
assert o.help == 'test option help\n (all|none|comma-separated list of names)\n allowed names: one two three', repr(o.help)
4040
assert o.default == 'all', o.default
41-
assert o.validator is None, o.validator
41+
assert o.validator is not None, o.validator
4242
assert o.converter is not None, o.converter
4343

4444
opts = SCons.Variables.Variables()
@@ -52,9 +52,15 @@ def test_ListVariable(self) -> None:
5252
def test_converter(self) -> None:
5353
"""Test the ListVariable converter"""
5454
opts = SCons.Variables.Variables()
55-
opts.Add(SCons.Variables.ListVariable('test', 'test option help', 'all',
56-
['one', 'two', 'three'],
57-
{'ONE':'one', 'TWO':'two'}))
55+
opts.Add(
56+
SCons.Variables.ListVariable(
57+
'test',
58+
'test option help',
59+
'all',
60+
['one', 'two', 'three'],
61+
{'ONE': 'one', 'TWO': 'two'},
62+
)
63+
)
5864

5965
o = opts.options[0]
6066

@@ -101,8 +107,12 @@ def test_converter(self) -> None:
101107
x = o.converter('three,ONE,TWO')
102108
assert str(x) == 'all', x
103109

104-
with self.assertRaises(ValueError):
105-
x = o.converter('no_match')
110+
# invalid value should convert (no change) without error
111+
x = o.converter('no_match')
112+
assert str(x) == 'no_match', x
113+
# ... and fail to validate
114+
with self.assertRaises(SCons.Errors.UserError):
115+
z = o.validator('test', 'no_match', {"test": x})
106116

107117
def test_copy(self) -> None:
108118
"""Test copying a ListVariable like an Environment would"""

doc/man/scons.xml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5203,7 +5203,7 @@ converted to lower case.</para>
52035203
</varlistentry>
52045204

52055205
<varlistentry id="v-ListVariable">
5206-
<term><function>ListVariable</function>(<parameter>key, help, default, names, [map]</parameter>)</term>
5206+
<term><function>ListVariable</function>(<parameter>key, help, default, names, [map, validator]</parameter>)</term>
52075207
<listitem>
52085208
<para>
52095209
Set up a variable
@@ -5225,6 +5225,8 @@ separated by commas.
52255225
<parameter>default</parameter> may be specified
52265226
either as a string of comma-separated value,
52275227
or as a list of values.
5228+
</para>
5229+
<para>
52285230
The optional
52295231
<parameter>map</parameter>
52305232
argument is a dictionary
@@ -5236,6 +5238,14 @@ list.
52365238
(Note that the additional values accepted through
52375239
the use of a <parameter>map</parameter> are not
52385240
reflected in the generated help message). </para>
5241+
<para>
5242+
The optional <parameter>validator</parameter> argument
5243+
can be used to specify a custom validator callback function,
5244+
as described for <link linkend='v-Add'><function>Add</function></link>.
5245+
The default is to use an internal validator routine.
5246+
</para>
5247+
<para><emphasis>New in 4.8.0: <parameter>validator</parameter>.
5248+
</emphasis></para>
52395249
</listitem>
52405250
</varlistentry>
52415251

test/Variables/BoolVariable.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,7 @@ def check(expect):
3939

4040

4141
test.write(SConstruct_path, """\
42-
from SCons.Variables.BoolVariable import BoolVariable
43-
44-
BV = BoolVariable
45-
42+
from SCons.Variables.BoolVariable import BoolVariable as BV
4643
from SCons.Variables import BoolVariable
4744
4845
opts = Variables(args=ARGUMENTS)
@@ -51,7 +48,8 @@ def check(expect):
5148
BV('profile', 'create profiling informations', False),
5249
)
5350
54-
env = Environment(variables=opts)
51+
_ = DefaultEnvironment(tools=[])
52+
env = Environment(variables=opts, tools=[])
5553
Help(opts.GenerateHelpText(env))
5654
5755
print(env['warnings'])
@@ -69,7 +67,7 @@ def check(expect):
6967
expect_stderr = """
7068
scons: *** Error converting option: 'warnings'
7169
Invalid value for boolean variable: 'irgendwas'
72-
""" + test.python_file_line(SConstruct_path, 13)
70+
""" + test.python_file_line(SConstruct_path, 11)
7371

7472
test.run(arguments='warnings=irgendwas', stderr=expect_stderr, status=2)
7573

test/Variables/EnumVariable.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ def check(expect):
3838
assert result[1:len(expect)+1] == expect, (result[1:len(expect)+1], expect)
3939

4040
test.write(SConstruct_path, """\
41-
from SCons.Variables.EnumVariable import EnumVariable
42-
EV = EnumVariable
43-
41+
from SCons.Variables.EnumVariable import EnumVariable as EV
4442
from SCons.Variables import EnumVariable
4543
4644
list_of_libs = Split('x11 gl qt ical')
@@ -58,7 +56,8 @@ def check(expect):
5856
map={}, ignorecase=2), # make lowercase
5957
)
6058
61-
env = Environment(variables=opts)
59+
_ = DefaultEnvironment(tools=[])
60+
env = Environment(variables=opts, tools=[])
6261
Help(opts.GenerateHelpText(env))
6362
6463
print(env['debug'])
@@ -78,19 +77,19 @@ def check(expect):
7877

7978
expect_stderr = """
8079
scons: *** Invalid value for enum variable 'debug': 'FULL'. Valid values are: ('yes', 'no', 'full')
81-
""" + test.python_file_line(SConstruct_path, 21)
80+
""" + test.python_file_line(SConstruct_path, 20)
8281

8382
test.run(arguments='debug=FULL', stderr=expect_stderr, status=2)
8483

8584
expect_stderr = """
8685
scons: *** Invalid value for enum variable 'guilib': 'irgendwas'. Valid values are: ('motif', 'gtk', 'kde')
87-
""" + test.python_file_line(SConstruct_path, 21)
86+
""" + test.python_file_line(SConstruct_path, 20)
8887

8988
test.run(arguments='guilib=IrGeNdwas', stderr=expect_stderr, status=2)
9089

9190
expect_stderr = """
9291
scons: *** Invalid value for enum variable 'some': 'irgendwas'. Valid values are: ('xaver', 'eins')
93-
""" + test.python_file_line(SConstruct_path, 21)
92+
""" + test.python_file_line(SConstruct_path, 20)
9493

9594
test.run(arguments='some=IrGeNdwas', stderr=expect_stderr, status=2)
9695

0 commit comments

Comments
 (0)