Skip to content

Commit 45a9e25

Browse files
authored
Merge pull request SCons#4523 from mwichmann/maint/Variables-enum
Variables cleanup: EnumVariable
2 parents c0f742e + e55e87d commit 45a9e25

File tree

3 files changed

+91
-59
lines changed

3 files changed

+91
-59
lines changed

SCons/Variables/EnumVariable.py

Lines changed: 78 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -38,70 +38,110 @@
3838
ignorecase=2,
3939
)
4040
)
41-
...
41+
env = Environment(variables=opts)
4242
if env['debug'] == 'full':
43-
...
43+
...
4444
"""
4545

46-
from typing import Tuple, Callable
46+
from typing import Callable, List, Optional, Tuple
4747

4848
import SCons.Errors
4949

5050
__all__ = ['EnumVariable',]
5151

5252

5353
def _validator(key, val, env, vals) -> None:
54-
if val not in vals:
55-
raise SCons.Errors.UserError(
56-
'Invalid value for option %s: %s. Valid values are: %s' % (key, val, vals))
57-
54+
"""Validate that val is in vals.
5855
59-
def EnumVariable(key, help, default, allowed_values, map={}, ignorecase: int=0) -> Tuple[str, str, str, Callable, Callable]:
56+
Usable as the base for :class:`EnumVariable` validators.
57+
"""
58+
if val not in vals:
59+
msg = (
60+
f"Invalid value for enum variable {key!r}: {val!r}. "
61+
f"Valid values are: {vals}"
62+
)
63+
raise SCons.Errors.UserError(msg) from None
64+
65+
66+
# lint: W0622: Redefining built-in 'help' (redefined-builtin)
67+
# lint: W0622: Redefining built-in 'map' (redefined-builtin)
68+
def EnumVariable(
69+
key,
70+
help: str,
71+
default: str,
72+
allowed_values: List[str],
73+
map: Optional[dict] = None,
74+
ignorecase: int = 0,
75+
) -> Tuple[str, str, str, Callable, Callable]:
6076
"""Return a tuple describing an enumaration SCons Variable.
6177
62-
The input parameters describe an option with only certain values
63-
allowed. Returns A tuple including an appropriate converter and
64-
validator. The result is usable as input to :meth:`Add`.
65-
66-
*key* and *default* are passed directly on to :meth:`Add`.
67-
68-
*help* is the descriptive part of the help text,
69-
and will have the allowed values automatically appended.
78+
The input parameters describe a variable with only predefined values
79+
allowed. The value of *ignorecase* defines the behavior of the
80+
validator and converter: if ``0``, the validator/converter are
81+
case-sensitive; if ``1``, the validator/converter are case-insensitive;
82+
if ``2``, the validator/converter are case-insensitive and the
83+
converted value will always be lower-case.
84+
85+
Arguments:
86+
key: variable name, passed directly through to the return tuple.
87+
default: default values, passed directly through to the return tuple.
88+
help: descriptive part of the help text,
89+
will have the allowed values automatically appended.
90+
allowed_values: list of the allowed values for this variable.
91+
map: optional dictionary which may be used for converting the
92+
input value into canonical values (e.g. for aliases).
93+
ignorecase: defines the behavior of the validator and converter.
94+
validator: callback function to test whether the value is in the
95+
list of allowed values.
96+
converter: callback function to convert input values according to
97+
the given *map*-dictionary. Unmapped input values are returned
98+
unchanged.
99+
100+
Returns:
101+
A tuple including an appropriate converter and validator.
102+
The result is usable as input to :meth:`~SCons.Variables.Variables.Add`.
103+
and :meth:`~SCons.Variables.Variables.AddVariables`.
104+
"""
105+
# these are all inner functions so they can access EnumVariable locals.
106+
def validator_rcase(key, val, env):
107+
"""Case-respecting validator."""
108+
return _validator(key, val, env, allowed_values)
70109

71-
*allowed_values* is a list of strings, which are the allowed values
72-
for this option.
110+
def validator_icase(key, val, env):
111+
"""Case-ignoring validator."""
112+
return _validator(key, val.lower(), env, allowed_values)
73113

74-
The *map*-dictionary may be used for converting the input value
75-
into canonical values (e.g. for aliases).
114+
def converter_rcase(val):
115+
"""Case-respecting converter."""
116+
return map.get(val, val)
76117

77-
The value of *ignorecase* defines the behaviour of the validator:
118+
def converter_icase(val):
119+
"""Case-ignoring converter."""
120+
return map.get(val.lower(), val)
78121

79-
* 0: the validator/converter are case-sensitive.
80-
* 1: the validator/converter are case-insensitive.
81-
* 2: the validator/converter is case-insensitive and the
82-
converted value will always be lower-case.
122+
def converter_lcase(val):
123+
"""Case-lowering converter."""
124+
return map.get(val.lower(), val).lower()
83125

84-
The *validator* tests whether the value is in the list of allowed values.
85-
The *converter* converts input values according to the given
86-
*map*-dictionary (unmapped input values are returned unchanged).
87-
"""
126+
if map is None:
127+
map = {}
128+
help = f"{help} ({'|'.join(allowed_values)})"
88129

89-
help = '%s (%s)' % (help, '|'.join(allowed_values))
90130
# define validator
91131
if ignorecase:
92-
validator = lambda key, val, env: \
93-
_validator(key, val.lower(), env, allowed_values)
132+
validator = validator_icase
94133
else:
95-
validator = lambda key, val, env: \
96-
_validator(key, val, env, allowed_values)
134+
validator = validator_rcase
135+
97136
# define converter
98137
if ignorecase == 2:
99-
converter = lambda val: map.get(val.lower(), val).lower()
138+
converter = converter_lcase
100139
elif ignorecase == 1:
101-
converter = lambda val: map.get(val.lower(), val)
140+
converter = converter_icase
102141
else:
103-
converter = lambda val: map.get(val, val)
104-
return (key, help, default, validator, converter)
142+
converter = converter_rcase
143+
144+
return key, help, default, validator, converter
105145

106146
# Local Variables:
107147
# tab-width:4

SCons/Variables/EnumVariableTests.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,11 @@ def test_converter(self) -> None:
121121

122122
for k, l in table.items():
123123
x = o0.converter(k)
124-
assert x == l[0], "o0 got %s, expected %s" % (x, l[0])
124+
assert x == l[0], f"o0 got {x}, expected {l[0]}"
125125
x = o1.converter(k)
126-
assert x == l[1], "o1 got %s, expected %s" % (x, l[1])
126+
assert x == l[1], f"o1 got {x}, expected {l[1]}"
127127
x = o2.converter(k)
128-
assert x == l[2], "o2 got %s, expected %s" % (x, l[2])
128+
assert x == l[2], f"o2 got {x}, expected {l[2]}"
129129

130130
def test_validator(self) -> None:
131131
"""Test the EnumVariable validator"""
@@ -157,13 +157,11 @@ def valid(o, v) -> None:
157157
o.validator('X', v, {})
158158

159159
def invalid(o, v) -> None:
160-
caught = None
161-
try:
160+
with self.assertRaises(
161+
SCons.Errors.UserError,
162+
msg=f"did not catch expected UserError for o = {o.key}, v = {v}",
163+
):
162164
o.validator('X', v, {})
163-
except SCons.Errors.UserError:
164-
caught = 1
165-
assert caught, "did not catch expected UserError for o = %s, v = %s" % (o.key, v)
166-
167165
table = {
168166
'one' : [ valid, valid, valid],
169167
'One' : [invalid, valid, valid],

test/Variables/EnumVariable.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#!/usr/bin/env python
22
#
3-
# __COPYRIGHT__
3+
# MIT License
4+
#
5+
# Copyright The SCons Foundation
46
#
57
# Permission is hereby granted, free of charge, to any person obtaining
68
# a copy of this software and associated documentation files (the
@@ -20,15 +22,11 @@
2022
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
2123
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
2224
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23-
#
24-
25-
__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
2625

2726
"""
2827
Test the EnumVariable canned Variable type.
2928
"""
3029

31-
3230
import TestSCons
3331

3432
test = TestSCons.TestSCons()
@@ -39,8 +37,6 @@ def check(expect):
3937
result = test.stdout().split('\n')
4038
assert result[1:len(expect)+1] == expect, (result[1:len(expect)+1], expect)
4139

42-
43-
4440
test.write(SConstruct_path, """\
4541
from SCons.Variables.EnumVariable import EnumVariable
4642
EV = EnumVariable
@@ -72,7 +68,6 @@ def check(expect):
7268
Default(env.Alias('dummy', None))
7369
""")
7470

75-
7671
test.run(); check(['no', 'gtk', 'xaver'])
7772

7873
test.run(arguments='debug=yes guilib=Motif some=xAVER')
@@ -82,24 +77,23 @@ def check(expect):
8277
check(['full', 'KdE', 'eins'])
8378

8479
expect_stderr = """
85-
scons: *** Invalid value for option debug: FULL. Valid values are: ('yes', 'no', 'full')
80+
scons: *** Invalid value for enum variable 'debug': 'FULL'. Valid values are: ('yes', 'no', 'full')
8681
""" + test.python_file_line(SConstruct_path, 21)
8782

8883
test.run(arguments='debug=FULL', stderr=expect_stderr, status=2)
8984

9085
expect_stderr = """
91-
scons: *** Invalid value for option guilib: irgendwas. Valid values are: ('motif', 'gtk', 'kde')
86+
scons: *** Invalid value for enum variable 'guilib': 'irgendwas'. Valid values are: ('motif', 'gtk', 'kde')
9287
""" + test.python_file_line(SConstruct_path, 21)
9388

9489
test.run(arguments='guilib=IrGeNdwas', stderr=expect_stderr, status=2)
9590

9691
expect_stderr = """
97-
scons: *** Invalid value for option some: irgendwas. Valid values are: ('xaver', 'eins')
92+
scons: *** Invalid value for enum variable 'some': 'irgendwas'. Valid values are: ('xaver', 'eins')
9893
""" + test.python_file_line(SConstruct_path, 21)
9994

10095
test.run(arguments='some=IrGeNdwas', stderr=expect_stderr, status=2)
10196

102-
10397
test.pass_test()
10498

10599
# Local Variables:

0 commit comments

Comments
 (0)