Repository: SCons/scons

Issue:
Dictionary() implementation does not match documentation
As noted on https://pairlist4.pair.net/pipermail/scons-users/2018-July/007119.html, scons documents `Dictionary()` as returning a dict object whether or not it is called with arguments; however the implementation returns the environment's dictionary object if called without arguments, but returns a value (the value for the requested key) if called with a single key, and a list of values if called with multiple key arguments. 
This is the case for current git (3.0.1 < current < 3.1) and is not a recent change (most recent commit that touched it was 2010).

### Manpage:

 env.Dictionary([vars])

Returns a dictionary object containing copies of all of the construction variables in the environment. If there are any variable names specified, only the specified construction variables are returned in the dictionary.

**Example:**
```
    dict = env.Dictionary()
    cc_dict = env.Dictionary('CC', 'CCFLAGS', 'CCCOM')
```


But testing exactly that example -

**SConstruct:**
```
env = Environment()
dict = env.Dictionary()
print("dict: " + str(type(dict)))
cc_dict = env.Dictionary('CC', 'CCFLAGS', 'CCCOM')
print("cc_dict: " + str(type(cc_dict)))
print(cc_dict)
```

**Output:**
```
scons: Reading SConscript files ...
dict: <type 'dict'>
cc_dict: <type 'list'>
['gcc', [], '$CC -o $TARGET -c $CFLAGS $CCFLAGS $_CCCOMCOM $SOURCES']
scons: done reading SConscript files.
```



Discussion:
It is easy enough to fix `Dictionary(args)` to return a subset dict containing only the key or keys specified. This one-liner needs a try block because,  naturally, Python renamed something important between 2 and 3. Dict comprehension implies at least Py 2.7.

```
  try:
      dlist = {key: self._dict[key] for key in self._dict.viewkeys() & args}
  except AttributeError:  # python3
      dlist = {key: self._dict[key] for key in self._dict.keys() & args}
```
For user calls to `Dictionary(args)`, it is impossible to tell if there are any that would break, but we could always say "don't depend on behavior different than what is documented". It is suspected that users just index the environment, which behaves like a dictionary. That is, `env['FOO']` is more likely than `Dictionary('FOO')`; if someone actually uses Dictionary this way they probably (and correctly) would be doing `Dictionary()['FOO'].`

There is only one internal non-test call that uses this syntax (i.e. supplies arguments), and that is the `Dump()` function which just pretty-prints what it gets back from Dictionary, so it does not break.  Tests, on the other hand, seem to have an expectation of the current behavior.  Fixes are pretty simple: `assert Dictionary('FOO') == 'foo'` breaks, but can be replaced by either ``assert Dictionary()['FOO'] == 'foo'`` or ``assert Dictionary('FOO')['FOO'] == 'foo'``.  I'm not sure why the tests actually want to subset the dictionary as they often do, _except_ in the cases where they are testing the subset functionality itself. So I'm not sure which fix to choose, not being able to get into the minds of the original test authors.  

Three alternative approaches to fixing the code to behave like the method documentation:

1. deprecate `Dictionary`; stop using it in examples in the docs (there are only a few; manpage has two that are depending on the "incorrect" behavior). Don't actually see what purpose this serves as a public method since you can treat the environment like a dict. `Dictionary` doesn't ever have to vanish, but it could stop being listed as part of the API.
1. deprecate giving `Dictionary` arguments, leaving (after the deprecation period) the part that works as documented and eliminating the part that doesn't.
1. change the documentation to stop suggesting a dict is always returned; just tell the truth. There's not much point to returning a dictionary if you asked for just a few items anyway.

After this has sat for many months I think I'd lean towards alternative three, also sprinkling in some or all of alternative one - even if not formally deprecated, it really doesn't need to be presented in the docs as something to do.

Resolving patch:
diff --git a/src/engine/SCons/Environment.py b/src/engine/SCons/Environment.py
--- a/src/engine/SCons/Environment.py
+++ b/src/engine/SCons/Environment.py
@@ -1493,8 +1493,14 @@ def Decider(self, function):
 
         self.copy_from_cache = copy_function
 
+
     def Detect(self, progs):
         """Return the first available program in progs.
+
+        :param progs: one or more command names to check for
+        :type progs: str or list
+        :returns str: first name from progs that can be found.
+
         """
         if not SCons.Util.is_List(progs):
             progs = [ progs ]
@@ -1503,7 +1509,17 @@ def Detect(self, progs):
             if path: return prog
         return None
 
+
     def Dictionary(self, *args):
+        """Return construction variables from an environment.
+
+        :param *args: (optional) variable names to look up
+        :returns: if args omitted, the dictionary of all constr. vars.
+            If one arg, the corresponding value is returned.
+            If more than one arg, a list of values is returned.
+        :raises KeyError: if any of *args is not in the construction env.
+
+        """
         if not args:
             return self._dict
         dlist = [self._dict[x] for x in args]
@@ -1511,23 +1527,28 @@ def Dictionary(self, *args):
             dlist = dlist[0]
         return dlist
 
-    def Dump(self, key = None):
-        """
-        Using the standard Python pretty printer, return the contents of the
-        scons build environment as a string.
 
-        If the key passed in is anything other than None, then that will
-        be used as an index into the build environment dictionary and
-        whatever is found there will be fed into the pretty printer. Note
-        that this key is case sensitive.
+    def Dump(self, key=None):
+        """ Return pretty-printed string of construction variables.
+
+        :param key: if None, format the whole dict of variables.
+            Else look up and format just the value for key.
+
         """
         import pprint
         pp = pprint.PrettyPrinter(indent=2)
         if key:
-            dict = self.Dictionary(key)
+            cvars = self.Dictionary(key)
         else:
-            dict = self.Dictionary()
-        return pp.pformat(dict)
+            cvars = self.Dictionary()
+
+        # TODO: pprint doesn't do a nice job on path-style values
+        # if the paths contain spaces (i.e. Windows), because the
+        # algorithm tries to break lines on spaces, while breaking
+        # on the path-separator would be more "natural". Is there
+        # a better way to format those?
+        return pp.pformat(cvars)
+
 
     def FindIxes(self, paths, prefix, suffix):
         """


Test patch: