Repository: SCons/scons

Issue:
Variables instance bug
This issue was originally created at: 2009-07-04 00:35:03.
This issue was reported by: `simpleton`.
simpleton said at 2009-07-04 00:35:03
>I was browsing the Scons souce, and found this in `Variables/__init__.py`:
```python
        # create the singleton instance
        if is_global:
            self=Variables.instance
 
            if not Variables.instance:
                Variables.instance=self
```
>It seems to me that the line 'self=Variables.instance' is a bug.

gregnoel said at 2009-12-09 11:26:28
>Bug party triage.  Remove all uses of self.instance.




Discussion:
The usual SCons-y mess here.  The internal signature of the `Variables` *function* is:
```python
def Variables(files=None, args=ARGUMENTS):
```
The documented signature is:
```python
Variables([files, [args]])
```
The `Variables` class initializer, meanwhile, is defined this way:
```python
def __init__(self, files=None, args=None, is_global=True):
```
It I read that right, it means you could only instantiate a `Variables` if you directly called to it, passing `is_global=False`, you can't do that through the published API (the `Variables` function).  

So do I read the @gregnoel comment as: get rid of the singleton-forcing stuff, and potentially make it possible for there to be more than one Variables object?  If not, then the docs should possibly receive an update to show it's a singleton, the way is mentioned for `DefaultEnvironment`.  

And, yes, the way this is written is broken, it doesn't create a singleton.  I think the author was expecting to be able to morph `self` this way but it doesn't do what's expected, so we're not implementing a singleton now.
It looks like Variables.instance is a class variable and thus is a singleton.
Am I reading that wrong?
It turns out... yes - my first glance was also to think "oh, it's a singleton".  By the time you're in `__init__`, a new instance has already been created.  You then set `self` in `__init__` to the one saved, which presumably _intends_ to morph the new instance into the old one, but it's actually just assigning to the local `self` and nothing is then done with that. So it's similar to a common pattern, but not doing it right. Unless I'm reading it completely wrong... I can easily make it into one, but I'm not sure there's any benefit to it.
Swung back and checked - it's definitely producing different instances each time you instantiate Variables.  It's dead simple to fix - it's a small stanza in `__new__` instead of in `__init__`, but it doesn't really seem to matter, since we're already not getting a singleton and stuff still seems to be working.  This sort of jig would do it:
```python
    def __new__(cls):
        if not hasattr(cls, '_instance'):
            cls._instance = super().__new__(cls)
        return cls._instance
```

The bugparty note seemed to indicate a decision to just forget it: "Remove all uses of self.instance."

Resolving patch:
diff --git a/SCons/Variables/VariablesTests.py b/SCons/Variables/VariablesTests.py
--- a/SCons/Variables/VariablesTests.py
+++ b/SCons/Variables/VariablesTests.py
@@ -45,7 +45,7 @@ def __contains__(self, key) -> bool:
 
 
 def check(key, value, env) -> None:
-    assert int(value) == 6 * 9, "key %s = %s" % (key, repr(value))
+    assert int(value) == 6 * 9, f"key {key!r} = {value!r}"
 
 # Check saved option file by executing and comparing against
 # the expected dictionary
@@ -55,7 +55,7 @@ def checkSave(file, expected) -> None:
     with open(file, 'r') as f:
         exec(f.read(), gdict, ldict)
 
-    assert expected == ldict, "%s\n...not equal to...\n%s" % (expected, ldict)
+    assert expected == ldict, f"{expected}\n...not equal to...\n{ldict}"
 
 class VariablesTestCase(unittest.TestCase):
 
@@ -97,12 +97,9 @@ def test_Add(self) -> None:
         o.validator(o.key, o.converter(o.default), {})
 
         def test_it(var, opts=opts) -> None:
-            exc_caught = None
-            try:
+            with self.assertRaises(SCons.Errors.UserError):
                 opts.Add(var)
-            except SCons.Errors.UserError:
-                exc_caught = 1
-            assert exc_caught, "did not catch UserError for '%s'" % var
+
         test_it('foo/bar')
         test_it('foo-bar')
         test_it('foo.bar')
@@ -168,19 +165,12 @@ def test_Update(self) -> None:
 
         env = Environment()
         exc_caught = None
-        try:
+        with self.assertRaises(AssertionError):
             opts.Update(env)
-        except AssertionError:
-            exc_caught = 1
-        assert exc_caught, "did not catch expected assertion"
 
         env = Environment()
-        exc_caught = None
-        try:
+        with self.assertRaises(AssertionError):
             opts.Update(env, {})
-        except AssertionError:
-            exc_caught = 1
-        assert exc_caught, "did not catch expected assertion"
 
         # Test that a good value from the file is used and validated.
         test = TestSCons.TestSCons()
@@ -216,12 +206,8 @@ def test_Update(self) -> None:
                  lambda x: int(x) + 12)
 
         env = Environment()
-        exc_caught = None
-        try:
+        with self.assertRaises(AssertionError):
             opts.Update(env, {'ANSWER':'54'})
-        except AssertionError:
-            exc_caught = 1
-        assert exc_caught, "did not catch expected assertion"
 
         # Test that a good value from an args dictionary
         # passed to Update() is used and validated.
@@ -303,12 +289,8 @@ def test_args(self) -> None:
                  lambda x: int(x) + 12)
 
         env = Environment()
-        exc_caught = None
-        try:
+        with self.assertRaises(AssertionError):
             opts.Update(env)
-        except AssertionError:
-            exc_caught = 1
-        assert exc_caught, "did not catch expected assertion"
 
         # Test that a good (command-line) argument is used and validated.
         test = TestSCons.TestSCons()
@@ -466,7 +448,8 @@ def test_GenerateHelpText(self) -> None:
 """
 
         text = opts.GenerateHelpText(env)
-        assert text == expect, text
+        with self.subTest():
+            self.assertEqual(expect, text)
 
         expectAlpha = """
 A: a - alpha test
@@ -496,25 +479,26 @@ def test_GenerateHelpText(self) -> None:
     actual: 54
 """
         text = opts.GenerateHelpText(env, sort=cmp)
-        assert text == expectAlpha, text
+        with self.subTest():
+            self.assertEqual(expectAlpha, text)
 
         textBool = opts.GenerateHelpText(env, sort=True)
-        assert text == expectAlpha, text
+        with self.subTest():
+            self.assertEqual(expectAlpha, textBool)
 
         textBackwards = opts.GenerateHelpText(env, sort=lambda x, y: cmp(y, x))
-        assert textBackwards == expectBackwards, "Expected:\n%s\nGot:\n%s\n" % (
-            textBackwards,
-            expectBackwards,
-        )
+        with self.subTest():
+            self.assertEqual(expectBackwards, textBackwards)
 
     def test_FormatVariableHelpText(self) -> None:
         """Test generating custom format help text"""
         opts = SCons.Variables.Variables()
 
         def my_format(env, opt, help, default, actual, aliases) -> str:
-            return '%s %s %s %s %s\n' % (opt, default, actual, help, aliases)
+            return f'{opt} {default} {actual} {help} {aliases}\n'
 
-        opts.FormatVariableHelpText = my_format
+        _save = opts.FormatVariableHelpText
+        setattr(opts, 'FormatVariableHelpText', my_format)
 
         opts.Add('ANSWER',
                  'THE answer to THE question',
@@ -544,7 +528,8 @@ def my_format(env, opt, help, default, actual, aliases) -> str:
 """
 
         text = opts.GenerateHelpText(env)
-        assert text == expect, text
+        with self.subTest():
+            self.assertEqual(expect, text)
 
         expectAlpha = """\
 A 42 54 a - alpha test ['A']
@@ -552,7 +537,10 @@ def my_format(env, opt, help, default, actual, aliases) -> str:
 B 42 54 b - alpha test ['B']
 """
         text = opts.GenerateHelpText(env, sort=cmp)
-        assert text == expectAlpha, text
+        with self.subTest():
+            self.assertEqual(expectAlpha, text)
+
+        setattr(opts, 'FormatVariableHelpText', _save)
 
     def test_Aliases(self) -> None:
         """Test option aliases"""
@@ -567,12 +555,15 @@ def test_Aliases(self) -> None:
         env = Environment()
         opts.Update(env, {'ANSWER' : 'answer'})
 
-        assert 'ANSWER' in env
+        with self.subTest():
+            self.assertIn('ANSWER', env)
 
         env = Environment()
         opts.Update(env, {'ANSWERALIAS' : 'answer'})
 
-        assert 'ANSWER' in env and 'ANSWERALIAS' not in env
+        with self.subTest():
+            self.assertIn('ANSWER', env)
+            self.assertNotIn('ANSWERALIAS', env)
 
         # test alias as a list
         opts = SCons.Variables.Variables()
@@ -585,12 +576,15 @@ def test_Aliases(self) -> None:
         env = Environment()
         opts.Update(env, {'ANSWER' : 'answer'})
 
-        assert 'ANSWER' in env
+        with self.subTest():
+            self.assertIn('ANSWER', env)
 
         env = Environment()
         opts.Update(env, {'ANSWERALIAS' : 'answer'})
 
-        assert 'ANSWER' in env and 'ANSWERALIAS' not in env
+        with self.subTest():
+            self.assertIn('ANSWER', env)
+            self.assertNotIn('ANSWERALIAS', env)
 
 
 class UnknownVariablesTestCase(unittest.TestCase):
@@ -612,8 +606,8 @@ def test_unknown(self) -> None:
         opts.Update(env, args)
 
         r = opts.UnknownVariables()
-        assert r == {'UNKNOWN' : 'unknown'}, r
-        assert env['ANSWER'] == 'answer', env['ANSWER']
+        self.assertEqual({'UNKNOWN': 'unknown'}, r)
+        self.assertEqual('answer', env['ANSWER'])
 
     def test_AddOptionUpdatesUnknown(self) -> None:
         """Test updating of the 'unknown' dict"""
@@ -632,8 +626,9 @@ def test_AddOptionUpdatesUnknown(self) -> None:
         opts.Update(env,args)
 
         r = opts.UnknownVariables()
-        assert r == {'ADDEDLATER' : 'notaddedyet'}, r
-        assert env['A'] == 'a', env['A']
+        with self.subTest():
+            self.assertEqual({'ADDEDLATER': 'notaddedyet'}, r)
+            self.assertEqual('a', env['A'])
 
         opts.Add('ADDEDLATER',
                  'An option not present initially',
@@ -647,8 +642,9 @@ def test_AddOptionUpdatesUnknown(self) -> None:
         opts.Update(env, args)
 
         r = opts.UnknownVariables()
-        assert len(r) == 0, r
-        assert env['ADDEDLATER'] == 'added', env['ADDEDLATER']
+        with self.subTest():
+            self.assertEqual(0, len(r))
+            self.assertEqual('added', env['ADDEDLATER'])
 
     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
--- a/SCons/Variables/__init__.py
+++ b/SCons/Variables/__init__.py
@@ -21,60 +21,76 @@
 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
-"""Adds user-friendly customizable variables to an SCons build. """
+"""Adds user-friendly customizable variables to an SCons build."""
 
 import os.path
 import sys
 from functools import cmp_to_key
+from typing import Callable, Dict, List, Optional, Sequence, Union
 
 import SCons.Environment
 import SCons.Errors
 import SCons.Util
 import SCons.Warnings
 
-from .BoolVariable import BoolVariable  # okay
-from .EnumVariable import EnumVariable  # okay
-from .ListVariable import ListVariable  # naja
-from .PackageVariable import PackageVariable # naja
-from .PathVariable import PathVariable # okay
+# Note: imports are for the benefit of SCons.Main (and tests); since they
+#   are not used here, the "as Foo" form is for checkers.
+from .BoolVariable import BoolVariable as BoolVariable
+from .EnumVariable import EnumVariable as EnumVariable
+from .ListVariable import ListVariable as ListVariable
+from .PackageVariable import PackageVariable as PackageVariable
+from .PathVariable import PathVariable as PathVariable
 
+__all__ = [
+    "Variable",
+    "Variables",
+]
 
-class Variables:
-    """
-    Holds all the options, updates the environment with the variables,
-    and renders the help text.
+class Variable:
+    """A Build Variable."""
 
-    If *is_global* is true, this is a singleton, create only once.
+    __slots__ = ('key', 'aliases', 'help', 'default', 'validator', 'converter')
 
-    Args:
-      files (optional): List of option configuration files to load
-        (backward compatibility). If a single string is passed it is
-        automatically placed in a file list (Default value = None)
-      args (optional): dictionary to override values set from *files*.
-        (Default value = None)
-      is_global (optional): global instance? (Default value = True)
+    def __lt__(self, other):
+        """Comparison fuction so Variable instances sort."""
+        return self.key < other.key
+
+    def __str__(self) -> str:
+        """Provide a way to "print" a Variable object."""
+        return f"({self.key!r}, {self.aliases}, {self.help!r}, {self.default!r}, {self.validator}, {self.converter})"
 
+
+class Variables:
+    """A container for multiple Build Variables.
+
+    Includes methods to updates the environment with the variables,
+    and to render the help text.
+
+    Arguments:
+      files: string or list of strings naming variable config scripts
+         (default ``None``)
+      args: dictionary to override values set from *files*.  (default ``None``)
+      is_global: if true, return a global singleton Variables object 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).
+         *is_global* is deprecated.
     """
-    instance = None
 
-    def __init__(self, files=None, args=None, is_global: bool=True) -> None:
-        if args is None:
-            args = {}
-        self.options = []
-        self.args = args
+    def __init__(
+        self,
+        files: Optional[Union[str, Sequence[str]]] = None,
+        args: Optional[dict] = None,
+        is_global: bool = False,
+    ) -> None:
+        self.options: List[Variable] = []
+        self.args = args if args is not None else {}
         if not SCons.Util.is_List(files):
-            if files:
-                files = [files,]
-            else:
-                files = []
+            files = [files] if files else []
         self.files = files
-        self.unknown = {}
-
-        # create the singleton instance
-        if is_global:
-            self = Variables.instance
-            if not Variables.instance:
-                Variables.instance=self
+        self.unknown: Dict[str, str] = {}
 
     def __str__(self) -> str:
         """Provide a way to "print" a Variables object."""
@@ -85,18 +101,25 @@ def __str__(self) -> str:
         s += f"  args={self.args},\n  files={self.files},\n  unknown={self.unknown},\n)"
         return s
 
-    def _do_add(self, key, help: str="", default=None, validator=None, converter=None, **kwargs) -> None:
-
-        class Variable:
-            def __str__(self) -> str:
-                """Provide a way to "print" a Variable object."""
-                return f"({self.key!r}, {self.aliases}, {self.help!r}, {self.default!r}, {self.validator}, {self.converter})"
-
+    # lint: W0622: Redefining built-in 'help'
+    def _do_add(
+        self,
+        key: Union[str, List[str]],
+        help: str = "",
+        default=None,
+        validator: Optional[Callable] = None,
+        converter: Optional[Callable] = None,
+        **kwargs,
+    ) -> None:
+        """Create a Variable and add it to the list.
+
+        Internal routine, not public API.
+        """
         option = Variable()
 
         # If we get a list or a tuple, we take the first element as the
         # option key and store the remaining in aliases.
-        if SCons.Util.is_List(key) or SCons.Util.is_Tuple(key):
+        if SCons.Util.is_Sequence(key):
             option.key = key[0]
             option.aliases = list(key[1:])
         else:
@@ -104,7 +127,7 @@ def __str__(self) -> str:
             # TODO: normalize to not include key in aliases. Currently breaks tests.
             option.aliases = [key,]
         if not SCons.Environment.is_valid_construction_var(option.key):
-            raise SCons.Errors.UserError("Illegal Variables key `%s'" % str(option.key))
+            raise SCons.Errors.UserError(f"Illegal Variables key {option.key!r}")
         option.help = help
         option.default = default
         option.validator = validator
@@ -119,75 +142,74 @@ def __str__(self) -> str:
                 del self.unknown[alias]
 
     def keys(self) -> list:
-        """Returns the keywords for the options."""
-        return [o.key for o in self.options]
+        """Return the variable names."""
+        for option in self.options:
+            yield option.key
 
-    def Add(self, key, *args, **kwargs) -> None:
-        r""" Adds an option.
+    def Add(
+        self, key: Union[str, Sequence], *args, **kwargs,
+    ) -> None:
+        """Add a Build Variable.
 
         Arguments:
           key: the name of the variable, or a 5-tuple (or list).
-            If a tuple, and there are no additional arguments,
-            the tuple is unpacked into the four named kwargs from below.
-            If a tuple and there are additional arguments, the first word
-            of the tuple is taken as the key, and the remainder as aliases.
-          *args: optional positional arguments, corresponding to the four
-            named kwargs below.
+            If *key* is a tuple, and there are no additional positional
+            arguments, it is unpacked into the variable name plus the four
+            listed keyword arguments from below.
+            If *key* is a tuple and there are additional positional arguments,
+            the first word of the tuple is taken as the variable name,
+            and the remainder as aliases.
+          args: optional positional arguments, corresponding to the four
+            listed keyword arguments.
+          kwargs: arbitrary keyword arguments used by the variable itself.
 
         Keyword Args:
-          help: help text for the options (Default value = "")
-          default: default value for option (Default value = None)
-          validator: function called to validate the option's value
-            (Default value = None)
-          converter: function to be called to convert the option's
-            value before putting it in the environment. (Default value = None)
-          **kwargs: arbitrary keyword arguments used by the variable itself.
-
+          help: help text for the variable (default: ``""``)
+          default: default value for variable (default: ``None``)
+          validator: function called to validate the value (default: ``None``)
+          converter: function to be called to convert the variable's
+            value before putting it in the environment. (default: ``None``)
         """
-        if SCons.Util.is_List(key) or SCons.Util.is_Tuple(key):
+        if SCons.Util.is_Sequence(key):
             if not (len(args) or len(kwargs)):
                 return self._do_add(*key)
 
         return self._do_add(key, *args, **kwargs)
 
     def AddVariables(self, *optlist) -> None:
-        """ Adds a list of options.
+        """Add a list of Build Variables.
 
         Each list element is a tuple/list of arguments to be passed on
-        to the underlying method for adding options.
+        to the underlying method for adding variables.
 
         Example::
 
+            opt = Variables()
             opt.AddVariables(
                 ('debug', '', 0),
                 ('CC', 'The C compiler'),
                 ('VALIDATE', 'An option for testing validation', 'notset', validator, None),
             )
-
         """
+        for opt in optlist:
+            self._do_add(*opt)
 
-        for o in optlist:
-            self._do_add(*o)
-
-    def Update(self, env, args=None) -> None:
-        """ Updates an environment with the option variables.
+    def Update(self, env, args: Optional[dict] = None) -> None:
+        """Update an environment with the Build Variables.
 
         Args:
             env: the environment to update.
-            args (optional): a dictionary of keys and values to update
-                in *env*. If omitted, uses the variables from the commandline.
+            args: a dictionary of keys and values to update in *env*.
+               If omitted, uses the saved :attr:`args`
         """
+        # first pull in the defaults
+        values = {opt.key: opt.default for opt in self.options if opt.default is not None}
 
-        values = {}
-
-        # first set the defaults:
-        for option in self.options:
-            if option.default is not None:
-                values[option.key] = option.default
-
-        # next set the value specified in the options file
+        # next set the values specified in any options script(s)
         for filename in self.files:
+            # TODO: issue #816 use Node to access saved-variables file?
             if os.path.exists(filename):
+                # lint: W0622: Redefining built-in 'dir'
                 dir = os.path.split(os.path.abspath(filename))[0]
                 if dir:
                     sys.path.insert(0, dir)
@@ -225,43 +247,46 @@ def Update(self, env, args=None) -> None:
         # apply converters
         for option in self.options:
             if option.converter and option.key in values:
-                value = env.subst('${%s}'%option.key)
+                value = env.subst(f'${option.key}')
                 try:
                     try:
                         env[option.key] = option.converter(value)
                     except TypeError:
                         env[option.key] = option.converter(value, env)
-                except ValueError as x:
-                    raise SCons.Errors.UserError('Error converting option: %s\n%s'%(option.key, x))
-
+                except ValueError as exc:
+                    # We usually want the converter not to fail and leave
+                    # that to the validator, but in case, handle it.
+                    msg = f'Error converting option: {option.key!r}\n{exc}'
+                    raise SCons.Errors.UserError(msg) from exc
 
         # apply validators
         for option in self.options:
             if option.validator and option.key in values:
-                option.validator(option.key, env.subst('${%s}'%option.key), env)
+                option.validator(option.key, env.subst(f'${option.key}'), env)
 
     def UnknownVariables(self) -> dict:
-        """ Returns unknown variables.
+        """Return dict of unknown variables.
 
-        Identifies options that were not known, declared options in this object.
+        Identifies variables that were not recognized in this object.
         """
         return self.unknown
 
     def Save(self, filename, env) -> None:
-        """ Save the options to a file.
+        """Save the variables to a script.
 
-        Saves all the options which have non-default settings
-        to the given file as Python expressions.  This file can
-        then be used to load the options for a subsequent run.
-        This can be used to create an option cache file.
+        Saves all the variables which have non-default settings
+        to the given file as Python expressions.  This script can
+        then be used to load the variables for a subsequent run.
+        This can be used to create a build variable "cache" or
+        capture different configurations for selection.
 
         Args:
             filename: Name of the file to save into
-            env: the environment get the option values from
+            env: the environment to get the option values from
         """
-
         # Create the file and write out the header
         try:
+            # TODO: issue #816 use Node to access saved-variables file?
             with open(filename, 'w') as fh:
                 # Make an assignment in the file for each option
                 # within the environment that was assigned a value
@@ -278,7 +303,7 @@ def Save(self, filename, env) -> None:
                                 eval(repr(value))
                             except KeyboardInterrupt:
                                 raise
-                            except:
+                            except Exception:
                                 # Convert stuff that has a repr() that
                                 # cannot be evaluated into a string
                                 value = SCons.Util.to_String(value)
@@ -292,53 +317,72 @@ def Save(self, filename, env) -> None:
                             except TypeError:
                                 defaultVal = option.converter(defaultVal, env)
 
-                        if str(env.subst('${%s}' % option.key)) != str(defaultVal):
-                            fh.write('%s = %s\n' % (option.key, repr(value)))
+                        if str(env.subst(f'${option.key}')) != str(defaultVal):
+                            fh.write(f'{option.key} = {value!r}\n')
                     except KeyError:
                         pass
-        except OSError as x:
-            raise SCons.Errors.UserError('Error writing options to file: %s\n%s' % (filename, x))
+        except OSError as exc:
+            msg = f'Error writing options to file: {filename}\n{exc}'
+            raise SCons.Errors.UserError(msg) from exc
 
-    def GenerateHelpText(self, env, sort=None) -> str:
-        """ Generates the help text for the options.
+    def GenerateHelpText(self, env, sort: Union[bool, Callable] = False) -> str:
+        """Generate the help text for the Variables object.
 
         Args:
             env: an environment that is used to get the current values
-                of the options.
+                of the variables.
             sort: Either a comparison function used for sorting
-                (must take two arguments and return -1, 0 or 1)
+                (must take two arguments and return ``-1``, ``0`` or ``1``)
                 or a boolean to indicate if it should be sorted.
         """
-
+        # TODO the 'sort' argument matched the old way Python's sorted()
+        #   worked, taking a comparison function argument. That has been
+        #   removed so now we have to convert to a key.
         if callable(sort):
             options = sorted(self.options, key=cmp_to_key(lambda x, y: sort(x.key, y.key)))
+
         elif sort is True:
-            options = sorted(self.options, key=lambda x: x.key)
+            options = sorted(self.options)
         else:
             options = self.options
 
         def format_opt(opt, self=self, env=env) -> str:
             if opt.key in env:
-                actual = env.subst('${%s}' % opt.key)
+                actual = env.subst(f'${opt.key}')
             else:
                 actual = None
-            return self.FormatVariableHelpText(env, opt.key, opt.help, opt.default, actual, opt.aliases)
-
-        lines = [_f for _f in map(format_opt, options) if _f]
-        return ''.join(lines)
+            return self.FormatVariableHelpText(
+                env, opt.key, opt.help, opt.default, actual, opt.aliases
+            )
+        return ''.join(_f for _f in map(format_opt, options) if _f)
 
     fmt = '\n%s: %s\n    default: %s\n    actual: %s\n'
     aliasfmt = '\n%s: %s\n    default: %s\n    actual: %s\n    aliases: %s\n'
 
-    def FormatVariableHelpText(self, env, key, help, default, actual, aliases=None) -> str:
+    # lint: W0622: Redefining built-in 'help'
+    def FormatVariableHelpText(
+        self,
+        env,
+        key: str,
+        help: str,
+        default,
+        actual,
+        aliases: Optional[List[str]] = None,
+    ) -> str:
+        """Format the help text for a single variable.
+
+        The caller is responsible for obtaining all the values,
+        although now the :class:`Variable` class is more publicly exposed,
+        this method could easily do most of that work - however
+        that would change the existing published API.
+        """
         if aliases is None:
             aliases = []
         # Don't display the key name itself as an alias.
         aliases = [a for a in aliases if a != key]
         if aliases:
             return self.aliasfmt % (key, help, default, actual, aliases)
-        else:
-            return self.fmt % (key, help, default, actual)
+        return self.fmt % (key, help, default, actual)
 
 # Local Variables:
 # tab-width:4


Test patch:
diff --git a/test/Variables/BoolVariable.py b/test/Variables/BoolVariable.py
--- a/test/Variables/BoolVariable.py
+++ b/test/Variables/BoolVariable.py
@@ -67,7 +67,7 @@ def check(expect):
 check([str(False), str(True)])
 
 expect_stderr = """
-scons: *** Error converting option: warnings
+scons: *** Error converting option: 'warnings'
 Invalid value for boolean variable: 'irgendwas'
 """ + test.python_file_line(SConstruct_path, 13)
 
diff --git a/test/Variables/ListVariable.py b/test/Variables/ListVariable.py
--- a/test/Variables/ListVariable.py
+++ b/test/Variables/ListVariable.py
@@ -113,7 +113,7 @@ def check(expect):
 
 
 expect_stderr = """
-scons: *** Error converting option: shared
+scons: *** Error converting option: 'shared'
 Invalid value(s) for option: foo
 """ + test.python_file_line(SConstruct_path, 20)
 
@@ -122,28 +122,28 @@ def check(expect):
 # be paranoid in testing some more combinations
 
 expect_stderr = """
-scons: *** Error converting option: shared
+scons: *** Error converting option: 'shared'
 Invalid value(s) for option: foo
 """ + test.python_file_line(SConstruct_path, 20)
 
 test.run(arguments='shared=foo,ical', stderr=expect_stderr, status=2)
 
 expect_stderr = """
-scons: *** Error converting option: shared
+scons: *** Error converting option: 'shared'
 Invalid value(s) for option: foo
 """ + test.python_file_line(SConstruct_path, 20)
 
 test.run(arguments='shared=ical,foo', stderr=expect_stderr, status=2)
 
 expect_stderr = """
-scons: *** Error converting option: shared
+scons: *** Error converting option: 'shared'
 Invalid value(s) for option: foo
 """ + test.python_file_line(SConstruct_path, 20)
 
 test.run(arguments='shared=ical,foo,x11', stderr=expect_stderr, status=2)
 
 expect_stderr = """
-scons: *** Error converting option: shared
+scons: *** Error converting option: 'shared'
 Invalid value(s) for option: foo,bar
 """ + test.python_file_line(SConstruct_path, 20)
 
diff --git a/test/Variables/Variables.py b/test/Variables/Variables.py
--- a/test/Variables/Variables.py
+++ b/test/Variables/Variables.py
@@ -27,7 +27,7 @@
 
 test = TestSCons.TestSCons()
 
-test.write('SConstruct', """
+test.write('SConstruct', """\
 DefaultEnvironment(tools=[])  # test speedup
 env = Environment()
 print(env['CC'])
@@ -37,20 +37,18 @@
 test.run()
 cc, ccflags = test.stdout().split('\n')[1:3]
 
-test.write('SConstruct', """
+test.write('SConstruct', """\
 # test validator.  Change a key and add a new one to the environment
 def validator(key, value, environ):
     environ[key] = "v"
     environ["valid_key"] = "v"
 
-
-def old_converter (value):
+def old_converter(value):
     return "old_converter"
 
-def new_converter (value, env):
+def new_converter(value, env):
     return "new_converter"
 
-
 opts = Variables('custom.py')
 opts.Add('RELEASE_BUILD',
          'Set to 1 to build a release build',
@@ -90,15 +88,17 @@ def new_converter (value, env):
 
 def test_tool(env):
     if env['RELEASE_BUILD']:
-        env.Append(CCFLAGS = '-O')
+        env.Append(CCFLAGS='-O')
     if env['DEBUG_BUILD']:
-        env.Append(CCFLAGS = '-g')
-
+        env.Append(CCFLAGS='-g')
 
 DefaultEnvironment(tools=[])  # test speedup
 env = Environment(variables=opts, tools=['default', test_tool])
 
-Help('Variables settable in custom.py or on the command line:\\n' + opts.GenerateHelpText(env))
+Help(
+    'Variables settable in custom.py or on the command line:\\n'
+    + opts.GenerateHelpText(env)
+)
 
 print(env['RELEASE_BUILD'])
 print(env['DEBUG_BUILD'])
@@ -121,7 +121,6 @@ def test_tool(env):
 assert env['RELEASE_BUILD'] == r
 
 Default(env.Alias('dummy', None))
-
 """)
 
 def check(expect):
@@ -146,7 +145,7 @@ def check(expect):
 test.run(arguments='CCFLAGS=--taco')
 check(['0', '1', cc, (ccflags + ' -g').strip(), 'v', 'v'])
 
-test.write('custom.py', """
+test.write('custom.py', """\
 DEBUG_BUILD=0
 RELEASE_BUILD=1
 """)
@@ -157,8 +156,7 @@ def check(expect):
 test.run(arguments='DEBUG_BUILD=1')
 check(['1', '1', cc, (ccflags + ' -O -g').strip(), 'v', 'v'])
 
-test.run(arguments='-h',
-         stdout = """\
+test.run(arguments='-h', stdout = """\
 scons: Reading SConscript files ...
 1
 0
@@ -220,7 +218,7 @@ def check(expect):
          'An option with no value')
 
 DefaultEnvironment(tools=[])  # test speedup
-env = Environment(variables = opts)
+env = Environment(variables=opts)
 
 print(env['RELEASE_BUILD'])
 print(env['DEBUG_BUILD'])
@@ -241,18 +239,18 @@ def checkSave(file, expected):
 # First test with no command line variables
 # This should just leave the custom.py settings
 test.run()
-check(['1','0'])
-checkSave('variables.saved', { 'RELEASE_BUILD':1, 'DEBUG_BUILD':0})
+check(['1', '0'])
+checkSave('variables.saved', {'RELEASE_BUILD': 1, 'DEBUG_BUILD': 0})
 
 # Override with command line arguments
 test.run(arguments='DEBUG_BUILD=3')
-check(['1','3'])
-checkSave('variables.saved', {'RELEASE_BUILD':1, 'DEBUG_BUILD':3})
+check(['1', '3'])
+checkSave('variables.saved', {'RELEASE_BUILD': 1, 'DEBUG_BUILD': 3})
 
 # Now make sure that saved variables are overridding the custom.py
 test.run()
-check(['1','3'])
-checkSave('variables.saved', {'DEBUG_BUILD':3, 'RELEASE_BUILD':1})
+check(['1', '3'])
+checkSave('variables.saved', {'DEBUG_BUILD': 3, 'RELEASE_BUILD': 1})
 
 # Load no variables from file(s)
 # Used to test for correct output in save option file
@@ -279,7 +277,7 @@ def checkSave(file, expected):
          names = ['a','b','c',])
 
 DefaultEnvironment(tools=[])  # test speedup
-env = Environment(variables = opts)
+env = Environment(variables=opts)
 
 print(env['RELEASE_BUILD'])
 print(env['DEBUG_BUILD'])
@@ -329,12 +327,13 @@ def checkSave(file, expected):
 def compare(a, b):
     return (a > b) - (a < b)
 
-Help('Variables settable in custom.py or on the command line:\\n' + opts.GenerateHelpText(env,sort=compare))
-
+Help(
+    'Variables settable in custom.py or on the command line:\\n'
+    + opts.GenerateHelpText(env, sort=compare)
+)
 """)
 
-test.run(arguments='-h',
-         stdout = """\
+test.run(arguments='-h', stdout = """\
 scons: Reading SConscript files ...
 scons: done reading SConscript files.
 Variables settable in custom.py or on the command line:
@@ -360,9 +359,10 @@ def compare(a, b):
 
 test.write('SConstruct', """
 import SCons.Variables
+
 DefaultEnvironment(tools=[])  # test speedup
-env1 = Environment(variables = Variables())
-env2 = Environment(variables = SCons.Variables.Variables())
+env1 = Environment(variables=Variables())
+env2 = Environment(variables=SCons.Variables.Variables())
 """)
 
 test.run()
diff --git a/test/Variables/chdir.py b/test/Variables/chdir.py
--- a/test/Variables/chdir.py
+++ b/test/Variables/chdir.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT License
+#
+# Copyright The SCons Foundation
 #
 # Permission is hereby granted, free of charge, to any person obtaining
 # a copy of this software and associated documentation files (the
@@ -20,9 +22,6 @@
 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
-
-__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
 """
 Verify that we can chdir() to the directory in which an Variables
diff --git a/test/Variables/help.py b/test/Variables/help.py
--- a/test/Variables/help.py
+++ b/test/Variables/help.py
@@ -101,7 +101,6 @@
 Default(env.Alias('dummy', None))
 """ % locals())
 
-
 test.run(arguments='-h',
          stdout = """\
 scons: Reading SConscript files ...
@@ -151,8 +150,6 @@
 Use scons -H for help about SCons built-in command-line options.
 """ % locals())
 
-
-
 test.pass_test()
 
 # Local Variables:
diff --git a/test/Variables/import.py b/test/Variables/import.py
--- a/test/Variables/import.py
+++ b/test/Variables/import.py
@@ -1,6 +1,8 @@
 #!/usr/bin/env python
 #
-# __COPYRIGHT__
+# MIT License
+#
+# Copyright The SCons Foundation
 #
 # Permission is hereby granted, free of charge, to any person obtaining
 # a copy of this software and associated documentation files (the
@@ -20,9 +22,6 @@
 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
-
-__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
 """
 Verify that an Variables file in a different directory can import