Merge pull request #4677 from mwichmann/issue/debug-memoizer
[scons.git] / SCons / Variables / __init__.py
blob2ac95d0673aa11c629b4f651fa95c59395309fb6
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 """Adds user-friendly customizable variables to an SCons build."""
26 from __future__ import annotations
28 import os.path
29 import sys
30 from contextlib import suppress
31 from dataclasses import dataclass
32 from functools import cmp_to_key
33 from typing import Any, Callable, Sequence
35 import SCons.Errors
36 import SCons.Util
37 import SCons.Warnings
39 # Note: imports are for the benefit of SCons.Main (and tests); since they
40 # are not used here, the "as Foo" form is for checkers.
41 from .BoolVariable import BoolVariable
42 from .EnumVariable import EnumVariable
43 from .ListVariable import ListVariable
44 from .PackageVariable import PackageVariable
45 from .PathVariable import PathVariable
47 __all__ = [
48 "Variable",
49 "Variables",
50 "BoolVariable",
51 "EnumVariable",
52 "ListVariable",
53 "PackageVariable",
54 "PathVariable",
58 @dataclass(order=True)
59 class Variable:
60 """A Build Variable."""
61 __slots__ = ('key', 'aliases', 'help', 'default', 'validator', 'converter', 'do_subst')
62 key: str
63 aliases: list[str]
64 help: str
65 default: Any
66 validator: Callable | None
67 converter: Callable | None
68 do_subst: bool
71 class Variables:
72 """A container for Build Variables.
74 Includes a method to populate the variables with values into a
75 construction envirionment, and methods to render the help text.
77 Note that the pubic API for creating a ``Variables`` object is
78 :func:`SCons.Script.Variables`, a kind of factory function, which
79 defaults to supplying the contents of :attr:`~SCons.Script.ARGUMENTS`
80 as the *args* parameter if it was not otherwise given. That is the
81 behavior documented in the manpage for ``Variables`` - and different
82 from the default if you instantiate this directly.
84 Arguments:
85 files: string or list of strings naming variable config scripts
86 (default ``None``)
87 args: dictionary to override values set from *files*. (default ``None``)
88 is_global: if true, return a global singleton :class:`Variables` object
89 instead of a fresh instance. Currently inoperable (default ``False``)
91 .. versionchanged:: 4.8.0
92 The default for *is_global* changed to ``False`` (the previous
93 default ``True`` had no effect due to an implementation error).
95 .. deprecated:: 4.8.0
96 *is_global* is deprecated.
98 .. versionadded:: NEXT_RELEASE
99 The :attr:`defaulted` attribute now lists those variables which
100 were filled in from default values.
103 def __init__(
104 self,
105 files: str | Sequence[str | None] = None,
106 args: dict | None = None,
107 is_global: bool = False,
108 ) -> None:
109 self.options: list[Variable] = []
110 self.args = args if args is not None else {}
111 if not SCons.Util.is_Sequence(files):
112 files = [files] if files else []
113 self.files: Sequence[str] = files
114 self.unknown: dict[str, str] = {}
115 self.defaulted: list[str] = []
117 def __str__(self) -> str:
118 """Provide a way to "print" a :class:`Variables` object."""
119 opts = ',\n'.join((f" {option!s}" for option in self.options))
120 return (
121 f"Variables(\n options=[\n{opts}\n ],\n"
122 f" args={self.args},\n"
123 f" files={self.files},\n"
124 f" unknown={self.unknown},\n"
125 f" defaulted={self.defaulted},\n)"
128 # lint: W0622: Redefining built-in 'help'
129 def _do_add(
130 self,
131 key: str | Sequence[str],
132 help: str = "",
133 default=None,
134 validator: Callable | None = None,
135 converter: Callable | None = None,
136 **kwargs,
137 ) -> None:
138 """Create a :class:`Variable` and add it to the list.
140 This is the internal implementation for :meth:`Add` and
141 :meth:`AddVariables`. Not part of the public API.
143 .. versionadded:: 4.8.0
144 *subst* keyword argument is now recognized.
146 # aliases needs to be a list for later concatenation operations
147 if SCons.Util.is_Sequence(key):
148 name, aliases = key[0], list(key[1:])
149 else:
150 name, aliases = key, []
151 if not name.isidentifier():
152 raise SCons.Errors.UserError(f"Illegal Variables key {name!r}")
153 do_subst = kwargs.pop("subst", True)
154 option = Variable(name, aliases, help, default, validator, converter, do_subst)
155 self.options.append(option)
157 # options might be added after the 'unknown' dict has been set up:
158 # look for and remove the key and all its aliases from that dict
159 for alias in option.aliases + [option.key,]:
160 if alias in self.unknown:
161 del self.unknown[alias]
163 def keys(self) -> list:
164 """Return the variable names."""
165 for option in self.options:
166 yield option.key
168 def Add(
169 self, key: str | Sequence, *args, **kwargs,
170 ) -> None:
171 """Add a Build Variable.
173 Arguments:
174 key: the name of the variable, or a 5-tuple (or other sequence).
175 If *key* is a tuple, and there are no additional arguments
176 except the *help*, *default*, *validator* and *converter*
177 keyword arguments, *key* is unpacked into the variable name
178 plus the *help*, *default*, *validator* and *converter*
179 arguments; if there are additional arguments, the first
180 elements of *key* is taken as the variable name, and the
181 remainder as aliases.
182 args: optional positional arguments, corresponding to the
183 *help*, *default*, *validator* and *converter* keyword args.
184 kwargs: arbitrary keyword arguments used by the variable itself.
186 Keyword Args:
187 help: help text for the variable (default: empty string)
188 default: default value for variable (default: ``None``)
189 validator: function called to validate the value (default: ``None``)
190 converter: function to be called to convert the variable's
191 value before putting it in the environment. (default: ``None``)
192 subst: perform substitution on the value before the converter
193 and validator functions (if any) are called (default: ``True``)
195 .. versionadded:: 4.8.0
196 The *subst* keyword argument is now specially recognized.
198 if SCons.Util.is_Sequence(key):
199 # If no other positional args (and no fundamental kwargs),
200 # unpack key, and pass the kwargs on:
201 known_kw = {'help', 'default', 'validator', 'converter'}
202 if not args and not known_kw.intersection(kwargs.keys()):
203 return self._do_add(*key, **kwargs)
205 return self._do_add(key, *args, **kwargs)
207 def AddVariables(self, *optlist) -> None:
208 """Add Build Variables.
210 Each *optlist* element is a sequence of arguments to be passed on
211 to the underlying method for adding variables.
213 Example::
215 opt = Variables()
216 opt.AddVariables(
217 ('debug', '', 0),
218 ('CC', 'The C compiler'),
219 ('VALIDATE', 'An option for testing validation', 'notset', validator, None),
222 for opt in optlist:
223 self._do_add(*opt)
225 def Update(self, env, args: dict | None = None) -> None:
226 """Update an environment with the Build Variables.
228 This is where the work of adding variables to the environment
229 happens, The input sources saved at init time are scanned for
230 variables to add, though if *args* is passed, then it is used
231 instead of the saved one. If any variable description set up
232 a callback for a validator and/or converter, those are called.
233 Variables from the input sources which do not match a variable
234 description in this object are ignored for purposes of adding
235 to *env*, but are saved in the :attr:`unknown` dict attribute.
236 Variables which are set in *env* from the default in a variable
237 description and not from the input sources are saved in the
238 :attr:`defaulted` list attribute.
240 Args:
241 env: the environment to update.
242 args: a dictionary of keys and values to update in *env*.
243 If omitted, uses the saved :attr:`args`
245 # first pull in the defaults, except any which are None.
246 values = {opt.key: opt.default for opt in self.options if opt.default is not None}
247 self.defaulted = list(values)
249 # next set the values specified in any saved-variables script(s)
250 for filename in self.files:
251 # TODO: issue #816 use Node to access saved-variables file?
252 if os.path.exists(filename):
253 # issue #4645: don't exec directly into values,
254 # so we can iterate through for unknown variables.
255 temp_values = {}
256 dirname = os.path.split(os.path.abspath(filename))[0]
257 if dirname:
258 sys.path.insert(0, dirname)
259 try:
260 temp_values['__name__'] = filename
261 with open(filename) as f:
262 contents = f.read()
263 exec(contents, {}, temp_values)
264 finally:
265 if dirname:
266 del sys.path[0]
267 del temp_values['__name__']
269 for arg, value in temp_values.items():
270 added = False
271 for option in self.options:
272 if arg in option.aliases + [option.key,]:
273 values[option.key] = value
274 with suppress(ValueError):
275 self.defaulted.remove(option.key)
276 added = True
277 if not added:
278 self.unknown[arg] = value
280 # set the values specified on the command line
281 if args is None:
282 args = self.args
284 for arg, value in args.items():
285 added = False
286 for option in self.options:
287 if arg in option.aliases + [option.key,]:
288 values[option.key] = value
289 with suppress(ValueError):
290 self.defaulted.remove(option.key)
291 added = True
292 if not added:
293 self.unknown[arg] = value
295 # put the variables in the environment
296 # (don't copy over variables that are not declared as options)
298 # Nitpicking: in OO terms, this method increases coupling as its
299 # main work is to update a different object (env), rather than
300 # the object it's bound to (although it does update self, too).
301 # It's tricky to decouple because the algorithm counts on directly
302 # setting a var in *env* first so it can call env.subst() on it
303 # to transform it.
305 for option in self.options:
306 try:
307 env[option.key] = values[option.key]
308 except KeyError:
309 pass
311 # apply converters
312 for option in self.options:
313 if option.converter and option.key in values:
314 if option.do_subst:
315 value = env.subst('${%s}' % option.key)
316 else:
317 value = env[option.key]
318 try:
319 try:
320 env[option.key] = option.converter(value)
321 except TypeError:
322 env[option.key] = option.converter(value, env)
323 except ValueError as exc:
324 # We usually want the converter not to fail and leave
325 # that to the validator, but in case, handle it.
326 msg = f'Error converting option: {option.key!r}\n{exc}'
327 raise SCons.Errors.UserError(msg) from exc
329 # apply validators
330 for option in self.options:
331 if option.validator and option.key in values:
332 if option.do_subst:
333 val = env[option.key]
334 if not SCons.Util.is_String(val):
335 # issue #4585: a _ListVariable should not be further
336 # substituted, breaks on values with spaces.
337 value = val
338 else:
339 value = env.subst('${%s}' % option.key)
340 else:
341 value = env[option.key]
342 option.validator(option.key, value, env)
344 def UnknownVariables(self) -> dict:
345 """Return dict of unknown variables.
347 Identifies variables that were not recognized in this object.
349 return self.unknown
351 def Save(self, filename, env) -> None:
352 """Save the variables to a script.
354 Saves all the variables which have non-default settings
355 to the given file as Python expressions. This script can
356 then be used to load the variables for a subsequent run.
357 This can be used to create a build variable "cache" or
358 capture different configurations for selection.
360 Args:
361 filename: Name of the file to save into
362 env: the environment to get the option values from
364 # Create the file and write out the header
365 try:
366 # TODO: issue #816 use Node to access saved-variables file?
367 with open(filename, 'w') as fh:
368 # Make an assignment in the file for each option
369 # within the environment that was assigned a value
370 # other than the default. We don't want to save the
371 # ones set to default: in case the SConscript settings
372 # change you would then pick up old defaults.
373 for option in self.options:
374 try:
375 value = env[option.key]
376 try:
377 prepare = value.prepare_to_store
378 except AttributeError:
379 try:
380 eval(repr(value))
381 except KeyboardInterrupt:
382 raise
383 except Exception:
384 # Convert stuff that has a repr() that
385 # cannot be evaluated into a string
386 value = SCons.Util.to_String(value)
387 else:
388 value = prepare()
390 defaultVal = env.subst(SCons.Util.to_String(option.default))
391 if option.converter:
392 try:
393 defaultVal = option.converter(defaultVal)
394 except TypeError:
395 defaultVal = option.converter(defaultVal, env)
397 if str(env.subst(f'${option.key}')) != str(defaultVal):
398 fh.write(f'{option.key} = {value!r}\n')
399 except KeyError:
400 pass
401 except OSError as exc:
402 msg = f'Error writing options to file: {filename}\n{exc}'
403 raise SCons.Errors.UserError(msg) from exc
405 def GenerateHelpText(self, env, sort: bool | Callable = False) -> str:
406 """Generate the help text for the Variables object.
408 Args:
409 env: an environment that is used to get the current values
410 of the variables.
411 sort: Either a comparison function used for sorting
412 (must take two arguments and return ``-1``, ``0`` or ``1``)
413 or a boolean to indicate if it should be sorted.
415 # TODO this interface was designed when Python's sorted() took an
416 # optional comparison function (pre-3.0). Since it no longer does,
417 # we use functools.cmp_to_key() since can't really change the
418 # documented meaning of the "sort" argument. Maybe someday?
419 if callable(sort):
420 options = sorted(self.options, key=cmp_to_key(lambda x, y: sort(x.key, y.key)))
421 elif sort is True:
422 options = sorted(self.options)
423 else:
424 options = self.options
426 def format_opt(opt, self=self, env=env) -> str:
427 if opt.key in env:
428 actual = env.subst(f'${opt.key}')
429 else:
430 actual = None
431 return self.FormatVariableHelpText(
432 env, opt.key, opt.help, opt.default, actual, opt.aliases
434 return ''.join(_f for _f in map(format_opt, options) if _f)
436 fmt = '\n%s: %s\n default: %s\n actual: %s\n'
437 aliasfmt = '\n%s: %s\n default: %s\n actual: %s\n aliases: %s\n'
439 # lint: W0622: Redefining built-in 'help'
440 def FormatVariableHelpText(
441 self,
442 env,
443 key: str,
444 help: str,
445 default,
446 actual,
447 aliases: list[str | None] = None,
448 ) -> str:
449 """Format the help text for a single variable.
451 The caller is responsible for obtaining all the values,
452 although now the :class:`Variable` class is more publicly exposed,
453 this method could easily do most of that work - however
454 that would change the existing published API.
456 if aliases is None:
457 aliases = []
458 # Don't display the key name itself as an alias.
459 aliases = [a for a in aliases if a != key]
460 if aliases:
461 return self.aliasfmt % (key, help, default, actual, aliases)
462 return self.fmt % (key, help, default, actual)
464 # Local Variables:
465 # tab-width:4
466 # indent-tabs-mode:nil
467 # End:
468 # vim: set expandtab tabstop=4 shiftwidth=4: