Merge pull request #4677 from mwichmann/issue/debug-memoizer
[scons.git] / SCons / Variables / ListVariable.py
blob880496f706aff2662d73ed8c5cd04adf01ac5081
1 # MIT License
3 # Copyright The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 """Variable type for List Variables.
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.
32 Usage example::
34 list_of_libs = Split('x11 gl qt ical')
36 opts = Variables()
37 opts.Add(
38 ListVariable(
39 'shared',
40 help='libraries to build as shared libraries',
41 default='all',
42 elems=list_of_libs,
45 env = Environment(variables=opts)
46 for lib in list_of_libs:
47 if lib in env['shared']:
48 env.SharedObject(...)
49 else:
50 env.Object(...)
51 """
53 # Known Bug: This should behave like a Set-Type, but does not really,
54 # since elements can occur twice.
56 from __future__ import annotations
58 import collections
59 import functools
60 from typing import Callable
62 import SCons.Util
64 __all__ = ['ListVariable',]
67 class _ListVariable(collections.UserList):
68 """Internal class holding the data for a List Variable.
70 This is normally not directly instantiated, rather the ListVariable
71 converter callback "converts" string input (or the default value
72 if none) into an instance and stores it.
74 Args:
75 initlist: the list of actual values given.
76 allowedElems: the list of allowable values.
77 """
79 def __init__(
80 self, initlist: list | None = None, allowedElems: list | None = None
81 ) -> None:
82 if initlist is None:
83 initlist = []
84 if allowedElems is None:
85 allowedElems = []
86 super().__init__([_f for _f in initlist if _f])
87 # TODO: why sorted? don't we want to display in the order user gave?
88 self.allowedElems = sorted(allowedElems)
90 def __cmp__(self, other):
91 return NotImplemented
93 def __eq__(self, other):
94 return NotImplemented
96 def __ge__(self, other):
97 return NotImplemented
99 def __gt__(self, other):
100 return NotImplemented
102 def __le__(self, other):
103 return NotImplemented
105 def __lt__(self, other):
106 return NotImplemented
108 def __str__(self) -> str:
109 if not self.data:
110 return 'none'
111 self.data.sort()
112 if self.data == self.allowedElems:
113 return 'all'
114 return ','.join(self)
116 def prepare_to_store(self):
117 return str(self)
119 def _converter(val, allowedElems, mapdict) -> _ListVariable:
120 """Callback to convert list variables into a suitable form.
122 The arguments *allowedElems* and *mapdict* are non-standard
123 for a :class:`Variables` converter: the lambda in the
124 :func:`ListVariable` function arranges for us to be called correctly.
126 Incoming values ``all`` and ``none`` are recognized and converted
127 into their expanded form.
129 if val == 'none':
130 val = []
131 elif val == 'all':
132 val = allowedElems
133 else:
134 val = [_f for _f in val.split(',') if _f]
135 val = [mapdict.get(v, v) for v in val]
136 return _ListVariable(val, allowedElems)
139 def _validator(key, val, env) -> None:
140 """Callback to validate supplied value(s) for a ListVariable.
142 Validation means "is *val* in the allowed list"? *val* has
143 been subject to substitution before the validator is called. The
144 converter created a :class:`_ListVariable` container which is stored
145 in *env* after it runs; this includes the allowable elements list.
146 Substitution makes a string made out of the values (only),
147 so we need to fish the allowed elements list out of the environment
148 to complete the validation.
150 Note that since 18b45e456, whether ``subst`` has been
151 called is conditional on the value of the *subst* argument to
152 :meth:`~SCons.Variables.Variables.Add`, so we have to account for
153 possible different types of *val*.
155 Raises:
156 UserError: if validation failed.
158 .. versionadded:: 4.8.0
159 ``_validator`` split off from :func:`_converter` with an additional
160 check for whether *val* has been substituted before the call.
162 allowedElems = env[key].allowedElems
163 if isinstance(val, _ListVariable): # not substituted, use .data
164 notAllowed = [v for v in val.data if v not in allowedElems]
165 else: # presumably a string
166 notAllowed = [v for v in val.split() if v not in allowedElems]
167 if notAllowed:
168 # Converter only synthesized 'all' and 'none', they are never
169 # in the allowed list, so we need to add those to the error message
170 # (as is done for the help msg).
171 valid = ','.join(allowedElems + ['all', 'none'])
172 msg = (
173 f"Invalid value(s) for variable {key!r}: {','.join(notAllowed)!r}. "
174 f"Valid values are: {valid}"
176 raise SCons.Errors.UserError(msg) from None
179 # lint: W0622: Redefining built-in 'help' (redefined-builtin)
180 # lint: W0622: Redefining built-in 'map' (redefined-builtin)
181 def ListVariable(
182 key,
183 help: str,
184 default: str | list[str],
185 names: list[str],
186 map: dict | None = None,
187 validator: Callable | None = None,
188 ) -> tuple[str, str, str, Callable, Callable]:
189 """Return a tuple describing a list variable.
191 A List Variable is an abstraction that allows choosing one or more
192 values from a provided list of possibilities (*names). The special terms
193 ``all`` and ``none`` are also provided to help make the selection.
195 Arguments:
196 key: the name of the list variable.
197 help: the basic help message. Will have text appended indicating
198 the allowed values (not including any extra names from *map*).
199 default: the default value(s) for the list variable. Can be given
200 as string (use commas to -separated multiple values), or as a list
201 of strings. ``all`` or ``none`` are allowed as *default*.
202 A must-specify ListVariable can be simulated by giving a value
203 that is not part of *names*, which will cause validation to fail
204 if the variable is not given in the input sources.
205 names: the values to choose from. Must be a list of strings.
206 map: optional dictionary to map alternative names to the ones in
207 *names*, providing a form of alias. The converter will make
208 the replacement, names from *map* are not stored and will
209 not appear in the help message.
210 validator: optional callback to validate supplied values.
211 The default validator is used if not specified.
213 Returns:
214 A tuple including the correct converter and validator. The
215 result is usable as input to :meth:`~SCons.Variables.Variables.Add`.
217 .. versionchanged:: 4.8.0
218 The validation step was split from the converter to allow for
219 custom validators. The *validator* keyword argument was added.
221 if map is None:
222 map = {}
223 if validator is None:
224 validator = _validator
225 names_str = f"allowed names: {' '.join(names)}"
226 if SCons.Util.is_List(default):
227 default = ','.join(default)
228 help = '\n '.join(
229 (help, '(all|none|comma-separated list of names)', names_str))
230 converter = functools.partial(_converter, allowedElems=names, mapdict=map)
231 return key, help, default, validator, converter
233 # Local Variables:
234 # tab-width:4
235 # indent-tabs-mode:nil
236 # End:
237 # vim: set expandtab tabstop=4 shiftwidth=4: