From eeb025f1591b0838f549b310883ab6a28eb42381 Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Mon, 25 Nov 2024 09:42:40 -0700 Subject: [PATCH] Add Variables.defualted attribute Also document the Variables.unknown attribute as being the same thing as the ruturn from the UnknownVariables method. Signed-off-by: Mats Wichmann --- CHANGES.txt | 8 +- RELEASE.txt | 6 ++ SCons/Variables/VariablesTests.py | 9 +- SCons/Variables/__init__.py | 67 ++++++++++----- doc/man/scons.xml | 131 ++++++++++++++++++------------ 5 files changed, 146 insertions(+), 75 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 54def0ad4..aa29180f5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -160,7 +160,13 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER variable names are given. - Update Clean and NoClean documentation. - Make sure unknown variables from a Variables file are recognized - as such (issue #4645) + as such. Previously only unknowns from the command line were + recognized (issue #4645). + - A Variables object now makes available a "defaulted" attribute, + a list of variable names that were set in the environment with + their values taken from the default in the variable description + (if a variable was set to the same value as the default in one + of the input sources, it is not included in this list). RELEASE 4.8.1 - Tue, 03 Sep 2024 17:22:20 -0700 diff --git a/RELEASE.txt b/RELEASE.txt index af5fb2fd2..aa3e3c15d 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -76,6 +76,12 @@ CHANGED/ENHANCED EXISTING FUNCTIONALITY always returns a dict. The default remains to return different types depending on whether zero, one, or multiple construction +- A Variables object now makes available a "defaulted" attribute, + a list of variable names that were set in the environment with + their values taken from the default in the variable description + (if a variable was set to the same value as the default in one + of the input sources, it is not included in this list). + FIXES ----- diff --git a/SCons/Variables/VariablesTests.py b/SCons/Variables/VariablesTests.py index 80c4d1139..bc981e06f 100644 --- a/SCons/Variables/VariablesTests.py +++ b/SCons/Variables/VariablesTests.py @@ -659,24 +659,28 @@ def test_AddOptionUpdatesUnknown(self) -> None: Get one unknown from args and one from a variables file. Add these later, making sure they no longer appear in unknowns after the subsequent Update(). + + While we're here, test the *defaulted* attribute. """ test = TestSCons.TestSCons() var_file = test.workpath('vars.py') test.write('vars.py', 'FROMFILE="added"') opts = SCons.Variables.Variables(files=var_file) - opts.Add('A', 'A test variable', "1") + opts.Add('A', 'A test variable', default="1") + opts.Add('B', 'Test variable B', default="1") args = { 'A' : 'a', 'ADDEDLATER' : 'notaddedyet', } env = Environment() - opts.Update(env,args) + opts.Update(env, args) r = opts.UnknownVariables() with self.subTest(): self.assertEqual('notaddedyet', r['ADDEDLATER']) self.assertEqual('added', r['FROMFILE']) self.assertEqual('a', env['A']) + self.assertEqual(['B'], opts.defaulted) opts.Add('ADDEDLATER', 'An option not present initially', "1") opts.Add('FROMFILE', 'An option from a file also absent', "1") @@ -693,6 +697,7 @@ def test_AddOptionUpdatesUnknown(self) -> None: self.assertEqual('added', env['ADDEDLATER']) self.assertNotIn('FROMFILE', r) self.assertEqual('added', env['FROMFILE']) + self.assertEqual(['B'], opts.defaulted) def test_AddOptionWithAliasUpdatesUnknown(self) -> None: """Test updating of the 'unknown' dict (with aliases)""" diff --git a/SCons/Variables/__init__.py b/SCons/Variables/__init__.py index 1826c6444..de26e7b65 100644 --- a/SCons/Variables/__init__.py +++ b/SCons/Variables/__init__.py @@ -27,6 +27,7 @@ import os.path import sys +from contextlib import suppress from functools import cmp_to_key from typing import Callable, Sequence @@ -58,22 +59,30 @@ class Variable: __slots__ = ('key', 'aliases', 'help', 'default', 'validator', 'converter', 'do_subst') def __lt__(self, other): - """Comparison fuction so Variable instances sort.""" + """Comparison fuction so :class:`Variable` instances sort.""" return self.key < other.key def __str__(self) -> str: - """Provide a way to "print" a Variable object.""" + """Provide a way to "print" a :class:`Variable` object.""" return ( - f"({self.key!r}, {self.aliases}, {self.help!r}, {self.default!r}, " + f"({self.key!r}, {self.aliases}, " + f"help={self.help!r}, default={self.default!r}, " f"validator={self.validator}, converter={self.converter})" ) class Variables: - """A container for multiple Build Variables. + """A container for Build Variables. - Includes methods to updates the environment with the variables, - and to render the help text. + Includes a method to populate the variables with values into a + construction envirionment, and methods to render the help text. + + Note that the pubic API for creating a ``Variables`` object is + :func:`SCons.Script.Variables`, a kind of factory function, which + defaults to supplying the contents of :attr:`~SCons.Script.ARGUMENTS` + as the *args* parameter if it was not otherwise given. That is the + behavior documented in the manpage for ``Variables`` - and different + from the default if you instantiate this directly. Arguments: files: string or list of strings naming variable config scripts @@ -83,11 +92,15 @@ class Variables: instead of a fresh instance. Currently inoperable (default ``False``) .. versionchanged:: 4.8.0 - The default for *is_global* changed to ``False`` (previously - ``True`` but it had no effect due to an implementation error). + The default for *is_global* changed to ``False`` (the previous + default ``True`` had no effect due to an implementation error). .. deprecated:: 4.8.0 *is_global* is deprecated. + + .. versionadded:: NEXT_RELEASE + The :attr:`defaulted` attribute now lists those variables which + were filled in from default values. """ def __init__( @@ -102,15 +115,18 @@ def __init__( files = [files] if files else [] self.files: Sequence[str] = files self.unknown: dict[str, str] = {} + self.defaulted: list[str] = [] def __str__(self) -> str: - """Provide a way to "print" a Variables object.""" - s = "Variables(\n options=[\n" - for option in self.options: - s += f" {str(option)},\n" - s += " ],\n" - s += f" args={self.args},\n files={self.files},\n unknown={self.unknown},\n)" - return s + """Provide a way to "print" a :class:`Variables` object.""" + opts = ',\n'.join((f" {option!s}" for option in self.options)) + return ( + f"Variables(\n options=[\n{opts}\n ],\n" + f" args={self.args},\n" + f" files={self.files},\n" + f" unknown={self.unknown},\n" + f" defaulted={self.defaulted},\n)" + ) # lint: W0622: Redefining built-in 'help' def _do_add( @@ -122,7 +138,7 @@ def _do_add( converter: Callable | None = None, **kwargs, ) -> None: - """Create a Variable and add it to the list. + """Create a :class:`Variable` and add it to the list. This is the internal implementation for :meth:`Add` and :meth:`AddVariables`. Not part of the public API. @@ -203,9 +219,9 @@ def Add( return self._do_add(key, *args, **kwargs) def AddVariables(self, *optlist) -> None: - """Add a list of Build Variables. + """Add Build Variables. - Each list element is a tuple/list of arguments to be passed on + Each *optlist* element is a sequence of arguments to be passed on to the underlying method for adding variables. Example:: @@ -223,13 +239,22 @@ def AddVariables(self, *optlist) -> None: def Update(self, env, args: dict | None = None) -> None: """Update an environment with the Build Variables. + Collects variables from the input sources which do not match + a variable description in this object. These are ignored for + purposes of adding to *env*, but can be retrieved using the + :meth:`UnknownVariables` method. Also collects variables which + are set in *env* from the default in a variable description and + not from the input sources. These are available in the + :attr:`defaulted` attribute. + Args: env: the environment to update. args: a dictionary of keys and values to update in *env*. If omitted, uses the saved :attr:`args` """ - # first pull in the defaults + # first pull in the defaults, except any which are None. values = {opt.key: opt.default for opt in self.options if opt.default is not None} + self.defaulted = list(values) # next set the values specified in any options script(s) for filename in self.files: @@ -256,6 +281,8 @@ def Update(self, env, args: dict | None = None) -> None: for option in self.options: if arg in option.aliases + [option.key,]: values[option.key] = value + with suppress(ValueError): + self.defaulted.remove(option.key) added = True if not added: self.unknown[arg] = value @@ -269,6 +296,8 @@ def Update(self, env, args: dict | None = None) -> None: for option in self.options: if arg in option.aliases + [option.key,]: values[option.key] = value + with suppress(ValueError): + self.defaulted.remove(option.key) added = True if not added: self.unknown[arg] = value diff --git a/doc/man/scons.xml b/doc/man/scons.xml index 8366bbd92..1b7e886c6 100644 --- a/doc/man/scons.xml +++ b/doc/man/scons.xml @@ -4704,47 +4704,52 @@ env = conf.Finish() Command-Line Construction Variables -Often when building software, -specialized information needs to be conveyed at build time -to override the defaults in the build scripts. -Command-line arguments (like --implcit-cache) -and giving names of build targets are two ways to do that. -Another is to provide variable-assignment arguments -on the command line. -For the particular case where you want to specify new + +&SCons; depends on information stored in &consvars; to +control how targets are built. +It is often necessary to pass +specialized information at build time +to override the variables in the build scripts. +This can be done through variable-assignment arguments +on the command line and/or in stored variable files. + + + +For the case where you want to specify new values for &consvars;, &SCons; provides a &Variables; object to simplify collecting those and updating a &consenv; with the values. -The typical calling style looks like: +This helps processing commands lines like this: -scons VARIABLE=foo +scons VARIABLE=foo OTHERVAR=bar -Variables specified in the above way -can be manually processed by accessing the +Variables supplied on the command line +can always be manually processed by iterating the &ARGUMENTS; dictionary -(or &ARGLIST; list), -but using a &Variables; object allows you to describe +or the &ARGLIST; list, +However, using a &Variables; object allows you to describe anticipated variables, -convert them to a suitable type if necessary, -validate the values are within defined constraints, -and define defaults, help messages and aliases. -This is conceptually similar to the structure of options +perform necessary type conversion, +validate that values meet defined constraints, +and specify default values, help messages and aliases. +This provides a somewhat similar interface to option handling (see &f-link-AddOption;). -It also allows obtaining values from a saved variables file, +A &Variables; object also allows +obtaining values from a saved variables file, or from a custom dictionary in an &SConscript; file. The processed variables can then be applied to the desired &consenv;. -Roughly speaking, arguments are used to convey information to the -&SCons; program about how it should behave; -variables are used to convey information to the build -(although &SCons; does not enforce any such constraint). +Conceptually, command-line targets control what to build, +command-line variables (and variable files) control how to build, +and command-line options control how &SCons; operates +(although &SCons; does not enforce that separation). To obtain an object for manipulating variables, @@ -4755,23 +4760,23 @@ call the &Variables; factory function: Variables([files, [args]]) If files is a filename or list of filenames, -they are considered to be &Python; scripts which will -be executed to set variables when the +they are executed as &Python; scripts +to set saved variables when the Update -method is called - -this allows the use of &Python; syntax in the assignments. -A file can be the result of an earlier call to the +method is called. +This allows the use of &Python; syntax in the assignments. +A variables file can be the result of an previous call to the &Save; method. If files is not specified, or the files argument is None, -then no files will be read. +then no files will be processed. Supplying None is required if there are no files but you want to specify args as a positional argument; -this can be omitted if using the keyword argument style. +or you can use keyword arguments to avoid that. If any of files is missing, it is silently skipped. @@ -4822,25 +4827,42 @@ vars = Variables(files=None, args=ARGUMENTS) -A &Variables; object serves as a container for -descriptions of variables, -which are added by calling methods of the object. -Each variable consists of a name (which will -become a &consvar;), aliases for the name, +A &Variables; object is a container for variable descriptions, +added by calling the +Add or +AddVariables +methods. +Each variable description consists of a name (which will +be used as the &consvar; name), aliases for the name, a help message, a default value, and functions to validate and convert values. -Once the object is asked to process variables, -it matches up data from the input -sources it was given with the definitions, -and generates key-value pairs which are added -to the specified &consenv;, -except that if any variable was described -to have a default of None, -it is not added to -the construction environment unless it -appears in the input sources. -Otherwise, a variable not in the -input sources is added using its default value. +Processing of input sources +is deferred until the +Update +method is called, +at which time the variables are added to the +specified &consenv;. +Variables from the input sources which do not match any +names or aliases from the variable descriptions in this object are skipped, +except that a dictionary of their names and values are made available +in the .unknown attribute of the &Variables; object. +This list can also be obtained via the +UnknownVariables +method. +If a variable description has a default value +other than None and does not +appear in the input sources, +it is added to the &consenv; with its default value. +A list of variables set from their defaults and +not supplied a value in the input sources is +available as the .defaulted attribute +of the &Variables; object. +The unknown variables and defaulted information is +not available until the &Update; method has run. + + +New in NEXT_RELEASE: +the defaulted attribute. @@ -4848,7 +4870,8 @@ Note that since the variables are eventually added as &consvars;, you should choose variable names which do not unintentionally change pre-defined &consvars; that your project will make use of (see for a reference), -since variables obtained have values overridden, not merged. +since the specified values are assigned, not merged, +to the respective &consvars;. @@ -5037,7 +5060,9 @@ variables that were specified in the files and/or args parameters when &Variables; -was called, but the object was not actually configured for. +was called, but which were not configured in the object. +The same dictionary is also available as the +unknown attribute of the object. This information is not available until the Update method has run. @@ -5148,7 +5173,7 @@ vars.FormatVariableHelpText = my_format &SCons; provides five pre-defined variable types, accessible through factory functions that generate a tuple appropriate for directly passing to the -Add +Add or AddVariables methods. @@ -5191,7 +5216,7 @@ as false. Set up a variable named key -whose value may only be from +whose value will be a choice from a specified list ("enumeration") of values. The variable will have a default value of default @@ -5234,8 +5259,8 @@ converted to lower case. Set up a variable named key -whose value may be one or more -from a specified list of values. +whose value will be one or more +choices from a specified list of values. The variable will have a default value of default, and help